CARLA
Loading...
Searching...
No Matches
WalkerBase.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
7#include "WalkerBase.h"
8#include "Carla.h"
11#include "Carla/Game/Tagger.h"
14
15#include <util/ue-header-guard-begin.h>
16#include "Animation/AnimSequence.h"
17#include "Components/CapsuleComponent.h"
18#include "Engine/SkeletalMesh.h"
19#include "Components/SkeletalMeshComponent.h"
20#include "PhysicsEngine/PhysicsAsset.h"
21#include "Engine/OverlapResult.h"
22#include "Engine/World.h"
23#include "GameFramework/CharacterMovementComponent.h"
24#include "TimerManager.h"
25#include <util/ue-header-guard-end.h>
26
27AWalkerBase::AWalkerBase(const FObjectInitializer &ObjectInitializer)
28 : Super(ObjectInitializer)
29{
30 PrimaryActorTick.bCanEverTick = true;
31}
32
34{
35 // Re-tag every component of the walker so a wheelchair mesh attached by
36 // the blueprint after spawn gets its semantic-segmentation stencil (a
37 // freshly attached component has none). Unlike ue4-dev this needs no
38 // episode lookup: the UE5 tagger derives the instance id from the actor
39 // itself. The wheelchair mesh lives under Carla/Static/Pedestrian, which
40 // ATagger::GetLabelByPath resolves to the Pedestrian label.
41 ATagger::TagActor(*this, true);
42}
43
45{
46 if (WheelchairComponent != nullptr)
47 {
48 return;
49 }
50 USkeletalMeshComponent *WalkerMesh = GetMesh();
51 if (WalkerMesh == nullptr)
52 {
53 UE_LOG(LogCarla, Warning,
54 TEXT("Wheelchair: walker %s has no mesh, spawning as normal pedestrian"),
55 *GetName());
56 return;
57 }
58
59 // Soft references throughout: a build without the wheelchair content must
60 // still run, degrading to a normal pedestrian.
61 auto *ChairMesh = Cast<USkeletalMesh>(FSoftObjectPath(
62 TEXT("/Game/Carla/Static/Pedestrian/Prop_Wheelchair/Meshes_/SM_Wheelchair.SM_Wheelchair"))
63 .TryLoad());
64 auto *SittingAnim = Cast<UAnimSequence>(FSoftObjectPath(
65 TEXT("/Game/Carla/Static/Pedestrian/Animations/WheelchairUser/WheelchairSitting_Moving2.WheelchairSitting_Moving2"))
66 .TryLoad());
67 if (ChairMesh == nullptr || SittingAnim == nullptr)
68 {
69 UE_LOG(LogCarla, Warning,
70 TEXT("Wheelchair: content not available (mesh=%d anim=%d), walker %s stays a normal pedestrian"),
71 ChairMesh != nullptr, SittingAnim != nullptr, *GetName());
72 return;
73 }
74
75 WheelchairComponent = NewObject<USkeletalMeshComponent>(this, TEXT("WheelchairMesh"));
76 WheelchairComponent->SetSkeletalMesh(ChairMesh);
77 // Same collision convention as the walker body: query-only Pawn geometry.
78 // Sensors (lidar, semantic raycasts) see the chair, but it can never
79 // physically push the character or catch on the ground.
80 WheelchairComponent->SetCollisionProfileName(TEXT("CharacterMesh"));
81 WheelchairComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
82 WheelchairComponent->SetCollisionObjectType(ECC_Pawn);
83 WheelchairComponent->RegisterComponent();
84 WheelchairComponent->AttachToComponent(
85 WalkerMesh, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
86 // The UE5 G2 walker meshes are authored with a different mesh-space yaw
87 // than the UE4 pedestrians the chair asset was aligned to; +90 puts the
88 // chair's front (casters, footplates) under the rider's feet. Calibrated
89 // empirically with a 0/90/180/270 sweep and side-view captures.
90 WheelchairComponent->SetRelativeRotation(FRotator(0.0f, 90.0f, 0.0f));
91
92 // The sitting pose's pelvis height was authored against the UE4 walker
93 // root convention, leaving the rider hovering above the seat on the UE5
94 // G2 meshes. Lower the rider's mesh into the chair and compensate the
95 // chair (attached to that same mesh) so its wheels stay on the ground.
96 // 25cm calibrated empirically (0-30cm sweep with side-view captures):
97 // pelvis on the cushion, feet on the footplates, no clipping.
98 constexpr float RiderDrop = 25.0f;
99 WalkerMesh->AddLocalOffset(FVector(0.0f, 0.0f, -RiderDrop));
100 WheelchairComponent->AddLocalOffset(FVector(0.0f, 0.0f, RiderDrop));
101
102 // Wheelchair animation: the migrated UE4 AnimBlueprint if it loads and
103 // compiled (it blends idle/rolling by speed), else a looping rolling
104 // animation.
105 const TCHAR *ChairAnimMode = TEXT("none");
106 auto *ChairAnimClass = Cast<UClass>(FSoftObjectPath(
107 TEXT("/Game/Carla/Static/Pedestrian/Animations/Wheelchair/ABP_WheelChair_MK.ABP_WheelChair_MK_C"))
108 .TryLoad());
109 if (ChairAnimClass != nullptr && !ChairAnimClass->HasAnyClassFlags(CLASS_NewerVersionExists))
110 {
111 WheelchairComponent->SetAnimInstanceClass(ChairAnimClass);
112 ChairAnimMode = TEXT("ABP_WheelChair_MK");
113 }
114 else if (auto *RollingAnim = Cast<UAnimSequence>(FSoftObjectPath(
115 TEXT("/Game/Carla/Static/Pedestrian/Animations/Wheelchair/Wheelchair_Rolling.Wheelchair_Rolling"))
116 .TryLoad()))
117 {
118 WheelchairComponent->PlayAnimation(RollingAnim, /*bLooping=*/true);
119 ChairAnimMode = TEXT("Wheelchair_Rolling loop");
120 }
121
122 // The rider: the UE4 wheelchair-user AnimBlueprints do not compile in UE5
123 // (they reference GEN2 animations that were not migrated), so play the
124 // sitting animation directly. The anim targets the old GEN2 skeleton
125 // (SK_Pedestrian_Generan), registered as compatible with the UE5 walker
126 // skeleton (Skel_Pedestrian_G2) during content migration.
127 WalkerMesh->PlayAnimation(SittingAnim, /*bLooping=*/true);
128
130
131 UE_LOG(LogCarla, Display,
132 TEXT("Wheelchair attached to walker %s (chair anim: %s, rider anim: WheelchairSitting_Moving2 loop)"),
133 *GetName(), ChairAnimMode);
134}
135
136/// Whether @a Walker mirrors a walker of a Multi-GPU primary: its fate (hits,
137/// death, ragdoll) is the primary's, never decided here.
138static bool AWalkerBase_IsMirror(const AActor &Walker)
139{
140 const UCarlaEpisode *Episode = UCarlaStatics::GetCurrentEpisode(Walker.GetWorld());
141 return (Episode != nullptr) && !Episode->bIsPrimaryServer;
142}
143
145{
146 Super::BeginPlay();
147 // A walker is a kinematic character: every interaction it needs is
148 // query-based (movement sweeps, sensor traces, the vehicle proximity
149 // poll below). Rigid-body collision on any of its primitives makes it
150 // an infinite-mass wall to simulating vehicles - one chassis contact
151 // in the frame before the kill fires is enough to bounce or launch the
152 // car, and several migrated walker blueprints kept QueryAndPhysics
153 // primitives. Clamp them all to query-only; Kill() re-enables physics
154 // explicitly when it ragdolls the mesh.
155 {
156 TArray<UPrimitiveComponent *> Primitives;
157 GetComponents<UPrimitiveComponent>(Primitives, false);
158 for (UPrimitiveComponent *Primitive : Primitives)
159 {
160 const ECollisionEnabled::Type Enabled = Primitive->GetCollisionEnabled();
161 if (Enabled == ECollisionEnabled::QueryAndPhysics)
162 {
163 Primitive->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
164 }
165 else if (Enabled == ECollisionEnabled::PhysicsOnly)
166 {
167 Primitive->SetCollisionEnabled(ECollisionEnabled::NoCollision);
168 }
169 // Several migrated walker meshes carry a WorldDynamic object type
170 // instead of Pawn. Vehicle suspension traces skip Pawn objects but
171 // must ride dynamic world geometry, so a WorldDynamic body part is
172 // an invisible ramp under a wheel (observed: the sphere-cast wheel
173 // climbing CharacterMesh0 and yawing the car through a hit). Every
174 // walker primitive is a body part: normalize to Pawn.
175 if (Primitive->GetCollisionObjectType() == ECC_WorldDynamic)
176 {
177 Primitive->SetCollisionObjectType(ECC_Pawn);
178 }
179 }
180 }
181 if (AWalkerBase_IsMirror(*this))
182 {
183 // No local death detection, nor the Blueprint triggers that kill on
184 // overlap: the primary's frames say when this walker dies.
185 TArray<UPrimitiveComponent *> Primitives;
186 GetComponents<UPrimitiveComponent>(Primitives, false);
187 for (UPrimitiveComponent *Primitive : Primitives)
188 {
189 Primitive->SetGenerateOverlapEvents(false);
190 }
191 return;
192 }
193 // Primary detection: the physical hit itself. The same contact also
194 // feeds the collision sensor, so clients keep receiving the collision
195 // event exactly like they always have; the capsule is un-walled right
196 // after that first contact frame, before the solver can launch the
197 // vehicle.
198 OnActorHit.AddDynamic(this, &AWalkerBase::OnBodyHit);
199 // Fallback: a cheap proximity poll on a timer (the walker blueprints
200 // disable actor tick for performance, so Tick() is not an option).
201 GetWorldTimerManager().SetTimer(
203 VehicleSensorPeriod, /*bLoop=*/true);
204}
205
207 FVector, const FHitResult &)
208{
209 if (bAlive)
210 {
211 TryKillFromVehicle(OtherActor);
212 }
213}
214
216{
217 ACarlaWheeledVehicle *Vehicle = Cast<ACarlaWheeledVehicle>(OtherActor);
218 if (Vehicle == nullptr)
219 {
220 return;
221 }
222 // Threshold on the vehicle's own speed: a relative-speed test gets
223 // polluted by the contact itself (a nudged walker picks up opposite
224 // velocity, inflating the relative measure past the threshold during a
225 // parking-speed push). A walker walking into a stationary vehicle
226 // stays alive either way.
227 const FVector ImpactVelocity = Vehicle->GetVelocity();
228 if (ImpactVelocity.Size() >= VehicleKillSpeed)
229 {
230 Kill(ImpactVelocity, Vehicle);
231 }
232}
233
235{
236 if (!bAlive)
237 {
238 GetWorldTimerManager().ClearTimer(VehicleSensorTimer);
239 return;
240 }
241 // Proximity query instead of component overlap events: it does not
242 // depend on any other component's GenerateOverlapEvents flag (the
243 // blueprint-era death triggers lost their event bindings in the UE5
244 // migration), and the margin fires it before the first physical
245 // contact between the vehicle and the kinematic capsule.
246 const UCapsuleComponent *Capsule = GetCapsuleComponent();
247 if (Capsule == nullptr)
248 {
249 return;
250 }
251 const float ContactRadius = Capsule->GetScaledCapsuleRadius() + VehicleSensorMargin;
252 const float HalfHeight = Capsule->GetScaledCapsuleHalfHeight();
253
254 // The effective poll interval can't be shorter than a frame (timers
255 // fire at most once per tick), and a fast vehicle crosses the whole
256 // contact shell between polls: at 107 km/h and 25 fps that is ~120 cm
257 // per frame against an 85 cm shell, so a pure proximity test tunnels
258 // straight through (observed: no kill, and the suspension raycasts
259 // launched the car off the corpse-to-be). The query is therefore sized
260 // for the fastest supported closing speed, and each found vehicle is
261 // tested against its predicted path over the next interval instead of
262 // its current position only.
263 const float Dt = FMath::Max(VehicleSensorPeriod, GetWorld()->GetDeltaSeconds());
264 const float QueryRadius =
265 ContactRadius + VehicleBodyAllowance + VehicleSensorSpeedBound * Dt * 1.25f;
266
267 TArray<FOverlapResult> Overlaps;
268 FCollisionObjectQueryParams ObjectParams;
269 ObjectParams.AddObjectTypesToQuery(ECC_Vehicle);
270 FCollisionQueryParams Params(SCENE_QUERY_STAT(WalkerVehicleSensor), false, this);
271 if (!GetWorld()->OverlapMultiByObjectType(
272 Overlaps,
273 GetActorLocation(),
274 FQuat::Identity,
275 ObjectParams,
276 FCollisionShape::MakeCapsule(QueryRadius, HalfHeight),
277 Params))
278 {
279 return;
280 }
281
282 const FVector WalkerLoc = GetActorLocation();
283 for (const FOverlapResult &Overlap : Overlaps)
284 {
285 ACarlaWheeledVehicle *Vehicle = Cast<ACarlaWheeledVehicle>(Overlap.GetActor());
286 UPrimitiveComponent *VehicleBody = Overlap.GetComponent();
287 if (Vehicle == nullptr || VehicleBody == nullptr)
288 {
289 continue;
290 }
291 const FVector Velocity = Vehicle->GetVelocity();
292 const float Speed = Velocity.Size();
293 if (Speed < VehicleKillSpeed)
294 {
295 continue;
296 }
297 // Direct contact: distance to the vehicle's actual collision
298 // geometry, same semantics the low-speed validation locked in.
299 FVector OnBody;
300 const float DistToBody =
301 VehicleBody->GetDistanceToCollision(WalkerLoc, OnBody);
302 bool bKill = DistToBody >= 0.0f && DistToBody <= ContactRadius;
303 if (!bKill)
304 {
305 // Tunneling guard: closest approach of the vehicle center's
306 // predicted path over the next poll interval. Direction matters -
307 // a fast car in the next lane passes wide and must not kill.
308 const FVector PathEnd = FVector(Vehicle->GetActorLocation()) + Velocity * Dt * 1.25f;
309 const FVector Closest = FMath::ClosestPointOnSegment(
310 WalkerLoc, Vehicle->GetActorLocation(), PathEnd);
311 bKill = FVector::Dist2D(Closest, WalkerLoc) <=
312 ContactRadius + VehicleBodyAllowance &&
313 FMath::Abs(Closest.Z - WalkerLoc.Z) <= HalfHeight + 200.0f;
314 }
315 if (bKill)
316 {
317 Kill(Velocity, Vehicle);
318 return;
319 }
320 }
321}
322
323void AWalkerBase::Kill(FVector ImpactVelocity, AActor *Killer)
324{
325 if (!bAlive || AWalkerBase_IsMirror(*this))
326 {
327 return;
328 }
329 bAlive = false;
330
331 // Stop the brain: abort navigation and drop any pending manual input.
332 // Further client controls are rejected upstream (FWalkerActor returns
333 // WalkerDead once bAlive is false).
334 if (auto *WalkerController = Cast<AWalkerController>(GetController()))
335 {
336 WalkerController->StopNavigation();
337 WalkerController->ApplyWalkerControl(FWalkerControl());
338 }
339 if (auto *Movement = GetCharacterMovement())
340 {
341 Movement->StopMovementImmediately();
342 Movement->DisableMovement();
343 }
344
345 // The capsule is query-only, but the vehicle wheel suspension traces
346 // are queries too - a blocking response is exactly what made cars ride
347 // up the capsule and spin. Dead walkers must be invisible to vehicle
348 // physics while staying visible to everything else (lidar, camera
349 // ground truth, the crowd).
350 if (auto *Capsule = GetCapsuleComponent())
351 {
352 Capsule->SetCollisionResponseToChannel(ECC_Vehicle, ECR_Ignore);
353 }
354
355 // The kill replaces the physical contact, so the contact's client
356 // contract must be honored here: collision sensors on the vehicle
357 // (scenario_runner infraction scoring) expect a collision event with
358 // the walker as the other actor.
359 if (Killer != nullptr)
360 {
361 FHitResult FakeHit(ForceInit);
362 FakeHit.HitObjectHandle = FActorInstanceHandle(this);
363 FakeHit.ImpactPoint = GetActorLocation();
364 FakeHit.ImpactNormal = (-ImpactVelocity).GetSafeNormal();
365 Killer->OnActorHit.Broadcast(Killer, this,
366 ImpactVelocity * 100.0f /*pseudo impulse, ~walker mass*/, FakeHit);
367 }
368
369 USkeletalMeshComponent *BodyMesh = GetMesh();
370 bool bDeterministic = true;
371 if (const UCarlaEpisode *Episode = UCarlaStatics::GetCurrentEpisode(GetWorld()))
372 {
373 bDeterministic = Episode->GetSettings().bDeterministicRagdolls;
374 }
375
376 if (BodyMesh != nullptr && !bDeterministic)
377 {
378 // Physics ragdoll carried by the impact. The vehicle-channel ignore
379 // must land between the profile switch (which resets per-channel
380 // overrides) and SetSimulatePhysics (which instantiates the per-bone
381 // bodies from the component's current collision state) - otherwise
382 // the wheels ride the simulating bodies and launch the car.
383 BodyMesh->SetCollisionProfileName(TEXT("Ragdoll"));
384 BodyMesh->SetCollisionResponseToChannel(ECC_Vehicle, ECR_Ignore);
385 BodyMesh->SetSimulatePhysics(true);
386 BodyMesh->SetAllPhysicsLinearVelocity(ImpactVelocity, false);
387 }
388 else if (BodyMesh != nullptr)
389 {
390 // Deterministic death: directional animation (the recorder can
391 // replay it bit-for-bit, unlike a physics ragdoll). Pick the
392 // animation from the impact direction in the walker's frame.
393 const FVector LocalDir =
394 GetActorTransform().InverseTransformVectorNoScale(
395 ImpactVelocity.GetSafeNormal2D());
396 const TCHAR *AnimName;
397 if (FMath::Abs(LocalDir.X) >= FMath::Abs(LocalDir.Y))
398 {
399 // Pushed forward -> falls on its front; pushed backward -> back.
400 AnimName = LocalDir.X >= 0.0f
401 ? TEXT("/Game/Carla/Animations/Death_Anim/Anim_Death_F_01.Anim_Death_F_01")
402 : TEXT("/Game/Carla/Animations/Death_Anim/Anim_Death_B_01.Anim_Death_B_01");
403 }
404 else
405 {
406 AnimName = LocalDir.Y >= 0.0f
407 ? TEXT("/Game/Carla/Animations/Death_Anim/Anim_Death_R_01.Anim_Death_R_01")
408 : TEXT("/Game/Carla/Animations/Death_Anim/Anim_Death_L_01.Anim_Death_L_01");
409 }
410 if (auto *DeathAnim = Cast<UAnimSequence>(
411 StaticLoadObject(UAnimSequence::StaticClass(), nullptr, AnimName)))
412 {
413 BodyMesh->PlayAnimation(DeathAnim, /*bLooping=*/false);
414 }
415 else
416 {
417 UE_LOG(LogCarla, Warning,
418 TEXT("Walker %s: death animation '%s' not found, using ragdoll"),
419 *GetName(), AnimName);
420 BodyMesh->SetCollisionProfileName(TEXT("Ragdoll"));
421 BodyMesh->SetCollisionResponseToChannel(ECC_Vehicle, ECR_Ignore);
422 BodyMesh->SetSimulatePhysics(true);
423 BodyMesh->SetAllPhysicsLinearVelocity(ImpactVelocity, false);
424 }
425 }
426
427 // The corpse mesh must be invisible to vehicle physics in the
428 // animation path too (wheel suspension raycasts are queries and the
429 // mesh response to the vehicle channel is Block by default).
430 if (BodyMesh != nullptr && !BodyMesh->IsSimulatingPhysics())
431 {
432 BodyMesh->SetCollisionResponseToChannel(ECC_Vehicle, ECR_Ignore);
433 }
434
435 UE_LOG(LogCarla, Log, TEXT("Walker %s killed by vehicle impact (%.0f cm/s)"),
436 *GetName(), ImpactVelocity.Size());
438}
static bool AWalkerBase_IsMirror(const AActor &Walker)
Whether Walker mirrors a walker of a Multi-GPU primary: its fate (hits, death, ragdoll) is the primar...
Base class for CARLA wheeled vehicles.
static void TagActor(const AActor &Actor, bool bTagForSemanticSegmentation)
Set the tag of an actor.
Definition Tagger.cpp:130
float VehicleBodyAllowance
Lateral clearance (cm) added on top of the capsule radius and sensor margin when testing a fast vehic...
Definition WalkerBase.h:75
float VehicleKillSpeed
A vehicle touching the walker while moving at least this fast (cm/s) kills it - ue4-dev parity,...
Definition WalkerBase.h:48
virtual void BeginPlay() override
FTimerHandle VehicleSensorTimer
Definition WalkerBase.h:128
void AttachWheelchair()
Attach the wheelchair prop to the walker and switch the walker to the sitting animation.
TObjectPtr< USkeletalMeshComponent > WheelchairComponent
The wheelchair mesh attached by AttachWheelchair(), null for walkers spawned without one.
Definition WalkerBase.h:114
void StartDeathLifeSpan()
Definition WalkerBase.h:84
void TryKillFromVehicle(AActor *OtherActor)
float VehicleSensorPeriod
Vehicle proximity poll period in seconds.
Definition WalkerBase.h:68
void TagWheelchair()
Re-tag this walker's components.
float VehicleSensorSpeedBound
Fastest closing speed (cm/s) the proximity query is sized for (default 300 km/h).
Definition WalkerBase.h:81
AWalkerBase(const FObjectInitializer &ObjectInitializer)
float VehicleSensorMargin
How far beyond the capsule radius the vehicle proximity check reaches (cm).
Definition WalkerBase.h:62
void OnBodyHit(AActor *SelfActor, AActor *OtherActor, FVector NormalImpulse, const FHitResult &Hit)
void Kill(FVector ImpactVelocity, AActor *Killer=nullptr)
Kill the walker as if run over: stop navigation and movement, stop blocking vehicles,...
void CheckVehicleImpact()
A simulation episode.
static UCarlaEpisode * GetCurrentEpisode(const UObject *WorldContextObject)