Files
gig-line/Game/GigLine/Source/GigLine/GigLineVehicle.cpp
T
zwitschi 1cddfbfc68 fix(vehicle): spawn on the road, not 2.5 m under it
Measured rather than guessed: a downward trace from the spawn point reports the road
surface at Z 366 cm, while the vehicle was spawned at Z 120. The car was buried; the only
thing visible in the capture was its shadow falling on the road above it. Spawn is now
Z 470, the camera aims at 430, and it settles at Z 470.

Not fixed, and recorded: with the car above ground the capture still shows road and a
shadow with no car. Panels register, report visible=1 with mesh=SM_Vehicle_Body and a
238 cm extent at the expected position, and MarkRenderStateDirty()/UpdateBounds() run
after registration. The shadow proves a collider is there; the visible geometry is not.

Screenshots/README.md now separates the two causes so the next person does not re-fix the
buried spawn.
2026-11-09 15:33:42 +01:00

289 lines
11 KiB
C++

#include "GigLineVehicle.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;
}
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 object.
Setup.WheelClass = UChaosVehicleWheel::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());
}
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: %d wheel objects after register, 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 (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;
}