The pawn was found again after a cleanup removed its staging, and the diagnostic answers the zero-wheel question with a bigger one: with the pawn present the actor is launched to Z 1993 cm - 18 m above the ground it stands at 150 - and still reports 0 wheels. GigLine vehicle diag: mesh=1 simulating=1 physasset=yes setups=4 wheels=0 GigLine vehicle: settled at Z 1993 cm, simulating=1 Wheels are only created inside CreateVehicle() when the physics state produced a vehicle, so the chassis body is the suspect rather than the wheel setup: PA_Vehicle_Sedan has one box on the root bone, and a body that starts intersecting geometry is the usual reason a vehicle pawn launches itself. Parked rather than shipped: the pawn is not staged, the visible car stays as panels, and the physics is 'Vehicle physics - Chaos rig' in TODO.md with the evidence and three concrete steps. The demo no longer carries an 18 m flying car in its captures.
360 lines
14 KiB
C++
360 lines
14 KiB
C++
#include "GigLineVehicle.h"
|
|
|
|
#include "ChaosVehicleWheel.h"
|
|
#include "ChaosWheeledVehicleMovementComponent.h"
|
|
#include "Components/SkeletalMeshComponent.h"
|
|
|
|
// From the Blender report (Art/blender/vehicle/report.json). Kept as named constants so a
|
|
// model change is a compile-time mismatch to fix, not a number buried in a setup call.
|
|
namespace GigLineVehicleDims
|
|
{
|
|
constexpr float WheelRadiusCm = 33.0f;
|
|
constexpr float WheelBaseCm = 278.0f;
|
|
constexpr float TrackCm = 156.0f;
|
|
}
|
|
|
|
UGigLineFrontWheel::UGigLineFrontWheel()
|
|
{
|
|
// Dimensions from the model's report: radius 33 cm, width 22 cm. Steering yes, drive no.
|
|
WheelRadius = 33.0f;
|
|
WheelWidth = 22.0f;
|
|
bAffectedBySteering = true;
|
|
bAffectedByEngine = false;
|
|
MaxSteerAngle = 35.0f;
|
|
SuspensionMaxRaise = 8.0f;
|
|
SuspensionMaxDrop = 8.0f;
|
|
}
|
|
|
|
UGigLineRearWheel::UGigLineRearWheel()
|
|
{
|
|
WheelRadius = 33.0f;
|
|
WheelWidth = 22.0f;
|
|
bAffectedBySteering = false;
|
|
bAffectedByEngine = true;
|
|
SuspensionMaxRaise = 8.0f;
|
|
SuspensionMaxDrop = 8.0f;
|
|
}
|
|
|
|
AGigLineVehicle::AGigLineVehicle()
|
|
{
|
|
PrimaryActorTick.bCanEverTick = true;
|
|
|
|
if (USkeletalMeshComponent* Body = GetMesh())
|
|
{
|
|
static ConstructorHelpers::FObjectFinder<USkeletalMesh> MeshFinder(
|
|
TEXT("/Game/Hollywood/Vehicle/SK_Vehicle_Sedan.SK_Vehicle_Sedan"));
|
|
if (MeshFinder.Succeeded())
|
|
{
|
|
Body->SetSkeletalMesh(MeshFinder.Object);
|
|
}
|
|
Body->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
|
Body->SetCollisionProfileName(TEXT("PhysicsActor"));
|
|
Body->SetSimulatePhysics(true);
|
|
}
|
|
|
|
if (UChaosWheeledVehicleMovementComponent* Movement =
|
|
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent()))
|
|
{
|
|
ConfigureWheels();
|
|
}
|
|
}
|
|
|
|
void AGigLineVehicle::BuildVisuals()
|
|
{
|
|
// One component per authored system: body, glass, front and rear lights, mirrors and
|
|
// the four wheels. Each points at the static asset imported from the same Blender
|
|
// build, so the render matches what the model tool produced.
|
|
struct FPanelSpec
|
|
{
|
|
const TCHAR* Path;
|
|
FVector Offset;
|
|
FRotator Rotation;
|
|
};
|
|
const float HalfBase = 139.0f; // wheelbase 278 cm / 2
|
|
const float HalfTrack = 81.0f; // track 162 cm / 2
|
|
const FPanelSpec Specs[] = {
|
|
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Body.SM_Vehicle_Body"), FVector::ZeroVector, FRotator::ZeroRotator},
|
|
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Glass.SM_Vehicle_Glass"), FVector::ZeroVector, FRotator::ZeroRotator},
|
|
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Light_Front.SM_Vehicle_Light_Front"), FVector::ZeroVector, FRotator::ZeroRotator},
|
|
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Light_Rear.SM_Vehicle_Light_Rear"), FVector::ZeroVector, FRotator::ZeroRotator},
|
|
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Mirrors.SM_Vehicle_Mirrors"), FVector::ZeroVector, FRotator::ZeroRotator},
|
|
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Wheel.SM_Vehicle_Wheel"), FVector(HalfBase, HalfTrack, -16.f), FRotator::ZeroRotator},
|
|
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Wheel.SM_Vehicle_Wheel"), FVector(HalfBase, -HalfTrack, -16.f), FRotator::ZeroRotator},
|
|
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Wheel.SM_Vehicle_Wheel"), FVector(-HalfBase, HalfTrack, -16.f), FRotator::ZeroRotator},
|
|
{TEXT("/Game/Hollywood/Vehicle/SM_Vehicle_Wheel.SM_Vehicle_Wheel"), FVector(-HalfBase, -HalfTrack, -16.f), FRotator::ZeroRotator},
|
|
};
|
|
|
|
int32 Index = 0;
|
|
for (const FPanelSpec& Spec : Specs)
|
|
{
|
|
if (UStaticMesh* Mesh = LoadObject<UStaticMesh>(nullptr, Spec.Path))
|
|
{
|
|
UStaticMeshComponent* Component = NewObject<UStaticMeshComponent>(this,
|
|
*FString::Printf(TEXT("Panel_%d"), Index));
|
|
// Attach to the actor root rather than to the physics mesh, so the visuals are
|
|
// independent of the simulated component's own state.
|
|
Component->SetupAttachment(GetRootComponent());
|
|
Component->SetStaticMesh(Mesh);
|
|
Component->SetRelativeLocation(Spec.Offset);
|
|
Component->SetRelativeRotation(Spec.Rotation);
|
|
Component->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
|
Component->SetMobility(EComponentMobility::Movable);
|
|
Component->RegisterComponent();
|
|
// Runtime-created components need their render state forced: without this the
|
|
// panels registered and reported visible but never reached the renderer.
|
|
Component->MarkRenderStateDirty();
|
|
Component->UpdateBounds();
|
|
Panels.Add(Component);
|
|
++Index;
|
|
}
|
|
}
|
|
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: %d visual panels attached"), Panels.Num());
|
|
for (int32 PanelIndex = 0; PanelIndex < Panels.Num(); ++PanelIndex)
|
|
{
|
|
const UStaticMeshComponent* Panel = Panels[PanelIndex];
|
|
if (!Panel)
|
|
{
|
|
continue;
|
|
}
|
|
const FVector Where = Panel->GetComponentLocation();
|
|
const FBoxSphereBounds B = Panel->Bounds;
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("GigLine vehicle: panel %d at (%.0f,%.0f,%.0f) visible=%d extent=%.0f mesh=%s"),
|
|
PanelIndex, Where.X, Where.Y, Where.Z,
|
|
Panel->IsVisible() ? 1 : 0, B.BoxExtent.GetMax(),
|
|
Panel->GetStaticMesh() ? *Panel->GetStaticMesh()->GetName() : TEXT("NONE"));
|
|
}
|
|
}
|
|
|
|
void AGigLineVehicle::ConfigureWheels()
|
|
{
|
|
UChaosWheeledVehicleMovementComponent* Movement =
|
|
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent());
|
|
if (!Movement)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// One entry per wheel bone, in the order the rig was built. The front pair steers;
|
|
// the rear pair drives, which is the simplest configuration that reads as a car.
|
|
struct FWheelSpec
|
|
{
|
|
const TCHAR* Bone;
|
|
bool bSteer;
|
|
bool bDrive;
|
|
};
|
|
static const FWheelSpec Specs[] = {
|
|
{TEXT("Wheel_FL"), true, false},
|
|
{TEXT("Wheel_FR"), true, false},
|
|
{TEXT("Wheel_RL"), false, true},
|
|
{TEXT("Wheel_RR"), false, true},
|
|
};
|
|
|
|
Movement->WheelSetups.Empty();
|
|
for (const FWheelSpec& Spec : Specs)
|
|
{
|
|
FChaosWheelSetup Setup;
|
|
// FChaosWheelSetup has only the class, the bone and an offset in 5.8 - there is no
|
|
// per-setup disable flag, and steering/drive flags live on the wheel class.
|
|
Setup.WheelClass = Spec.bSteer ? UGigLineFrontWheel::StaticClass()
|
|
: UGigLineRearWheel::StaticClass();
|
|
Setup.BoneName = FName(Spec.Bone);
|
|
Movement->WheelSetups.Add(Setup);
|
|
}
|
|
|
|
// Radius and stance for every wheel; the per-wheel subclass would normally carry
|
|
// these, so they are applied after the setups exist.
|
|
for (int32 Index = 0; Index < Movement->WheelSetups.Num(); ++Index)
|
|
{
|
|
UChaosVehicleWheel* Wheel = Movement->Wheels.IsValidIndex(Index)
|
|
? Movement->Wheels[Index] : nullptr;
|
|
if (!Wheel)
|
|
{
|
|
continue;
|
|
}
|
|
Wheel->WheelRadius = GigLineVehicleDims::WheelRadiusCm;
|
|
Wheel->WheelWidth = 22.0f;
|
|
Wheel->bAffectedBySteering = Specs[Index].bSteer;
|
|
Wheel->bAffectedByEngine = Specs[Index].bDrive;
|
|
}
|
|
}
|
|
|
|
void AGigLineVehicle::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
// The skeletal mesh is the physics driver only: its imported geometry is unusable
|
|
// (1 cm bounds), so it is hidden and the visible car comes from the static panels.
|
|
if (USkeletalMeshComponent* PhysicsMesh = GetMesh())
|
|
{
|
|
// Do not hide the physics mesh at all. It has no usable geometry (1 cm bounds), so
|
|
// it renders nothing either way, and every attempt to hide it also hid the visual
|
|
// panels - with and without propagation. Leaving it visible avoids the fight.
|
|
PhysicsMesh->SetCastShadow(false);
|
|
}
|
|
BuildVisuals();
|
|
|
|
// Where is the road, really? A downward trace from the vehicle found nothing, and the
|
|
// capture shows the car is not on screen at all - so measure the surface from above
|
|
// before assuming the spawn height is right.
|
|
if (UWorld* World = GetWorld())
|
|
{
|
|
const FVector From = GetActorLocation() + FVector(0.f, 0.f, 1000.f);
|
|
FHitResult Hit;
|
|
FCollisionQueryParams Q(TEXT("GigLineVehicleRoad"), false, this);
|
|
const bool bHit = World->LineTraceSingleByChannel(Hit, From,
|
|
From - FVector(0.f, 0.f, 4000.f), ECC_Visibility, Q);
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("GigLine vehicle: surface below spawn at Z %.0f cm (hit=%d, actor at %.0f)"),
|
|
bHit ? Hit.ImpactPoint.Z : -9999.f, bHit ? 1 : 0, GetActorLocation().Z);
|
|
}
|
|
|
|
// Wheel objects do not exist until the component is registered, so the per-wheel
|
|
// flags set in the constructor went nowhere - which is why the drive test measured
|
|
// zero speed with throttle held. Apply them again now that they exist, and give the
|
|
// engine a torque curve to actually make force.
|
|
if (UChaosWheeledVehicleMovementComponent* Movement =
|
|
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent()))
|
|
{
|
|
ConfigureWheels();
|
|
Movement->EngineSetup.MaxTorque = 500.0f;
|
|
Movement->EngineSetup.MaxRPM = 6000.0f;
|
|
Movement->EngineSetup.EngineIdleRPM = 900.0f;
|
|
Movement->TransmissionSetup.bUseAutomaticGears = true;
|
|
Movement->TransmissionSetup.FinalRatio = 3.5f;
|
|
Movement->SetThrottleInput(0.0f);
|
|
if (const USkeletalMeshComponent* BodyComp = GetMesh())
|
|
{
|
|
const USkeletalMesh* Asset = BodyComp->GetSkeletalMeshAsset();
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("GigLine vehicle: mesh=%s visible=%d hidden=%d bounds=%s"),
|
|
Asset ? *Asset->GetName() : TEXT("NONE"),
|
|
BodyComp->IsVisible() ? 1 : 0,
|
|
BodyComp->bHiddenInGame ? 1 : 0,
|
|
*BodyComp->Bounds.BoxExtent.ToString());
|
|
}
|
|
// Name the bones and check each setup against them: Chaos silently creates no wheel
|
|
// when a setup's bone is not on the mesh, which is how four setups produced zero
|
|
// wheels without a warning.
|
|
if (USkeletalMeshComponent* Body = GetMesh())
|
|
{
|
|
TArray<FName> Bones;
|
|
Body->GetBoneNames(Bones);
|
|
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: %d bones, e.g. %s"),
|
|
Bones.Num(),
|
|
Bones.Num() > 0 ? *Bones[FMath::Min(1, Bones.Num() - 1)].ToString() : TEXT("none"));
|
|
for (const FChaosWheelSetup& Setup : Movement->WheelSetups)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: setup bone '%s' index %d"),
|
|
*Setup.BoneName.ToString(), Body->GetBoneIndex(Setup.BoneName));
|
|
}
|
|
}
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("GigLine vehicle: %d wheel objects at BeginPlay (wheels are created later, "
|
|
"with the physics state), torque %.0f"),
|
|
Movement->Wheels.Num(), Movement->EngineSetup.MaxTorque);
|
|
}
|
|
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: spawned at (%.0f, %.0f, %.0f), %d wheel setups"),
|
|
GetActorLocation().X, GetActorLocation().Y, GetActorLocation().Z,
|
|
GetVehicleMovementComponent()
|
|
? Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent())
|
|
->WheelSetups.Num()
|
|
: 0);
|
|
}
|
|
|
|
void AGigLineVehicle::Tick(float DeltaSeconds)
|
|
{
|
|
Super::Tick(DeltaSeconds);
|
|
|
|
// One settled-state report: proves the physics body exists, is simulating, and has
|
|
// come to rest on the road rather than falling through it. Everything else about the
|
|
// driving behaviour is measured later; this is the "is it real" check.
|
|
// `-drivetest`: hold full throttle from 1 s to 4 s and report the result. A vehicle
|
|
// that spawns is not a vehicle that drives.
|
|
if (FParse::Param(FCommandLine::Get(), TEXT("drivetest")))
|
|
{
|
|
DriveElapsed += DeltaSeconds;
|
|
if (UChaosVehicleMovementComponent* Movement = GetVehicleMovementComponent())
|
|
{
|
|
if (DriveElapsed > 1.0f && DriveElapsed < 4.0f)
|
|
{
|
|
Movement->SetThrottleInput(1.0f);
|
|
}
|
|
else if (DriveElapsed >= 4.0f && !bDriveReported)
|
|
{
|
|
Movement->SetThrottleInput(0.0f);
|
|
bDriveReported = true;
|
|
const float SpeedCm = Movement->GetForwardSpeed();
|
|
const float TravelledM = FVector::Dist2D(GetActorLocation(), DriveStart) / 100.f;
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("GigLine vehicle DRIVETEST speed=%.0f cm/s (%.1f km/h), travelled %.1f m -> %s"),
|
|
SpeedCm, SpeedCm * 0.036f, TravelledM,
|
|
(SpeedCm > 200.f && TravelledM > 1.0f) ? TEXT("PASS") : TEXT("FAIL"));
|
|
}
|
|
}
|
|
}
|
|
if (DriveStart.IsZero() && DriveElapsed > 1.0f)
|
|
{
|
|
DriveStart = GetActorLocation();
|
|
}
|
|
|
|
// One-shot diagnostic: why zero wheels. Wheels are built in CreateVehicle(), which runs only
|
|
// when the physics state produced a vehicle, so the useful facts are whether the mesh is
|
|
// simulating and whether it carries a physics asset at all.
|
|
{
|
|
static bool bDiagnosed = false;
|
|
if (!bDiagnosed && SettleElapsed > 3.0f && FParse::Param(FCommandLine::Get(), TEXT("probes")))
|
|
{
|
|
bDiagnosed = true;
|
|
USkeletalMeshComponent* Body = GetMesh();
|
|
const UChaosWheeledVehicleMovementComponent* Wheeled =
|
|
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent());
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("GigLine vehicle diag: mesh=%d simulating=%d physasset=%s setups=%d wheels=%d "
|
|
"loc=%.0f,%.0f,%.0f"),
|
|
Body != nullptr,
|
|
Body && Body->IsSimulatingPhysics() ? 1 : 0,
|
|
Body && Body->GetBodySetup() ? TEXT("yes") : TEXT("no"),
|
|
Wheeled ? Wheeled->WheelSetups.Num() : -1,
|
|
Wheeled ? Wheeled->Wheels.Num() : -1,
|
|
GetActorLocation().X, GetActorLocation().Y, GetActorLocation().Z);
|
|
}
|
|
}
|
|
|
|
SettleElapsed += DeltaSeconds;
|
|
if (!bSettleReported && SettleElapsed > 3.0f)
|
|
{
|
|
bSettleReported = true;
|
|
if (const UChaosWheeledVehicleMovementComponent* Wheeled =
|
|
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent()))
|
|
{
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("GigLine vehicle: %d wheel objects once physics exists"),
|
|
Wheeled->Wheels.Num());
|
|
}
|
|
if (UWorld* World = GetWorld())
|
|
{
|
|
FHitResult Hit;
|
|
FCollisionQueryParams Q(TEXT("GigLineVehicleGround"), false, this);
|
|
const bool bGround = World->LineTraceSingleByChannel(Hit, GetActorLocation(),
|
|
GetActorLocation() - FVector(0.f, 0.f, 2000.f), ECC_Visibility, Q);
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("GigLine vehicle: ground at Z %.0f cm, actor %.0f cm, height above ground %.0f cm"),
|
|
bGround ? Hit.ImpactPoint.Z : -9999.f, GetActorLocation().Z,
|
|
bGround ? GetActorLocation().Z - Hit.ImpactPoint.Z : -9999.f);
|
|
}
|
|
const USkeletalMeshComponent* Body = GetMesh();
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("GigLine vehicle: settled at Z %.0f cm, simulating=%d, speed=%.0f cm/s"),
|
|
GetActorLocation().Z,
|
|
Body && Body->IsSimulatingPhysics() ? 1 : 0,
|
|
GetVehicleMovementComponent() ? GetVehicleMovementComponent()->GetForwardSpeed() : 0.f);
|
|
}
|
|
}
|
|
|
|
bool AGigLineVehicle::IsMoving() const
|
|
{
|
|
const UChaosVehicleMovementComponent* Movement = GetVehicleMovementComponent();
|
|
return Movement && FMath::Abs(Movement->GetForwardSpeed()) > 50.0f;
|
|
}
|