Chaos reads steering and drive flags from the wheel object a setup points at, and the first attempt pointed every setup at the base UChaosVehicleWheel. Dedicated front and rear classes carry the modelled dimensions (radius 33 cm, width 22 cm), steering on the front axle, drive on the rear, and the suspension travel - which is also how Unreal's own vehicle template is organised. Compiles clean. Not yet verified in a run: the drive test did not log the vehicle this time, so whether the wheel count changes from 0 is still unproven and recorded as such rather than claimed.
313 lines
12 KiB
C++
313 lines
12 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());
|
|
}
|
|
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;
|
|
}
|