Two measurement errors found and corrected before drawing any conclusion:
- the vehicle pawn had been dropped from the map by an earlier cleanup; it is staged again
- wheels are created in CreateVehicle(), i.e. when the physics state is built, so counting
them in BeginPlay always read zero regardless
Measured once physics exists, the count is still **0**, and the bone theory is dead: the
skeleton has Wheel_FL/FR/RL/RR at indices 2-5 and every setup resolves to its bone.
CreateWheels() runs only inside CreateVehicle() when PVehicleOutput is valid, so a zero count
means the physics state never produced a vehicle - the problem is upstream of the wheel config,
in the physics asset or the way the skeletal mesh binds it, not in the wheel setups.
Also logged at the settle check: the pawn's own downward trace reports ground at Z 152 while
the visible car's panels stand on a road of 366, so the pawn and the visible car may not be in
the same place at all - worth checking before anything else.
337 lines
13 KiB
C++
337 lines
13 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();
|
|
}
|
|
|
|
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;
|
|
}
|