feat(vehicle): Chaos vehicle spawns and simulates, physics asset generated from script

The physics asset could not be made from Python (PhysicsAssetFactory exposes no mesh
property), so a small editor-only C++ helper calls FPhysicsAssetUtils::CreateFromSkeletalMesh
- the same code path as the editor's Create Physics Asset action. It produces
PA_Vehicle_Sedan with 6 bodies and assigns it to SK_Vehicle_Sedan.

AGigLineVehicle is a Chaos wheeled pawn that configures its four wheels from the modelled
dimensions (radius 33 cm, wheelbase 278, track 156) and is staged in DemoMap. A run logs:
  spawned at (-12950, -900, 120), 4 wheel setups
  settled at Z 494 cm, simulating=1, speed=0 cm/s

That last line is the defect and it is not hidden: the vehicle rests 3.7 m above the road,
so the generated physics bodies do not match the mesh. Recorded in TODO.md with the next
step - inspect the bodies against the mesh, then either fix the creation params or author
two boxes and four wheel spheres explicitly.
This commit is contained in:
zwitschi committed 2026-11-09 15:14:49 +01:00
1 parent 2e0421480c
commit 4f3ed20e49
29 files changed
+328 -27

No files matched your search

@@ -14,5 +14,19 @@ public class GigLine : ModuleRules
"InputCore",
"EnhancedInput"
});
// Editor-only: the vehicle physics asset is generated from the skeletal mesh with
// FPhysicsAssetUtils, which lives in the Developer/PhysicsUtilities module. Chaos
// Vehicles also need the physics vehicle module at runtime.
if (Target.bBuildEditor)
{
PrivateDependencyModuleNames.AddRange(new string[]
{
"UnrealEd",
"PhysicsUtilities",
});
}
PrivateDependencyModuleNames.Add("ChaosVehicles");
}
}
@@ -0,0 +1,127 @@
#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::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();
ConfigureWheels();
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.
SettleElapsed += DeltaSeconds;
if (!bSettleReported && SettleElapsed > 3.0f)
{
bSettleReported = true;
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;
}
@@ -0,0 +1,41 @@
#pragma once
#include "CoreMinimal.h"
#include "WheeledVehiclePawn.h"
#include "GigLineVehicle.generated.h"
class UChaosWheeledVehicleMovementComponent;
/**
* The one drivable vehicle in the demo.
*
* A skeletal mesh with a root bone and four wheel bones, driven by Chaos. Every
* dimension it configures — wheel radius, wheelbase, track — comes from the Blender
* build's report rather than being typed twice: the numbers are repeated here as
* constants with the report path in the comment so a change to the model shows up as a
* mismatch instead of a silent drift.
*
* Model: `Art/blender/vehicle/gigline-sedan.blend`, report `Art/blender/vehicle/report.json`
* (4.72 x 1.80 x 1.44 m, wheelbase 2.78 m, track 1.62 m, wheel radius 0.33 m).
*/
UCLASS()
class GIGLINE_API AGigLineVehicle : public AWheeledVehiclePawn
{
GENERATED_BODY()
public:
AGigLineVehicle();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
/** True once the vehicle reports a forward speed above a walking pace. */
bool IsMoving() const;
private:
/** Configure the Chaos wheels from the modelled dimensions. */
void ConfigureWheels();
float SettleElapsed = 0.f;
bool bSettleReported = false;
};
@@ -0,0 +1,69 @@
#include "GigLineVehicleBuilder.h"
#if WITH_EDITOR
#include "AssetToolsModule.h"
#include "Engine/SkeletalMesh.h"
#include "PhysicsAssetUtils.h"
#include "PhysicsEngine/PhysicsAsset.h"
#include "UObject/Package.h"
#endif
UPhysicsAsset* UGigLineVehicleBuilder::BuildPhysicsAsset(USkeletalMesh* Mesh,
const FString& PackagePath,
const FString& AssetName)
{
#if WITH_EDITOR
if (!Mesh)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: no skeletal mesh given"));
return nullptr;
}
const FString FullName = PackagePath / AssetName;
UPackage* Package = CreatePackage(*FullName);
if (!Package)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: could not create package %s"), *FullName);
return nullptr;
}
UPhysicsAsset* PhysicsAsset = NewObject<UPhysicsAsset>(Package, *AssetName,
RF_Public | RF_Standalone | RF_Transactional);
if (!PhysicsAsset)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: could not create %s"), *FullName);
return nullptr;
}
// Boxes on the five bones: a root box and four wheel capsules is what a car needs;
// per-bone hulls from a 500-triangle mesh would add cost for nothing.
FPhysAssetCreateParams Params;
Params.MinBoneSize = 1.0f;
Params.GeomType = EFG_Box;
Params.bCreateConstraints = true;
// Every bone gets a body: the vehicle has exactly five bones (root plus four
// wheels) and each one needs collision, so filtering by bone size only produced a
// single root body.
Params.bBodyForAll = true;
Params.bWalkPastSmall = false;
Params.bDisableCollisionsByDefault = true;
FText Error;
const bool bOk = FPhysicsAssetUtils::CreateFromSkeletalMesh(
PhysicsAsset, Mesh, Params, Error, /*bSetToMesh=*/true, /*bShowProgress=*/false);
if (!bOk)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: physics asset build failed: %s"),
*Error.ToString());
return nullptr;
}
PhysicsAsset->MarkPackageDirty();
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: physics asset %s with %d bodies"),
*FullName, PhysicsAsset->SkeletalBodySetups.Num());
return PhysicsAsset;
#else
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: physics asset generation is editor-only"));
return nullptr;
#endif
}
@@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GigLineVehicleBuilder.generated.h"
class UPhysicsAsset;
class USkeletalMesh;
/**
* Script-callable helpers for vehicle setup.
*
* Chaos Vehicles need a PhysicsAsset, and Unreal's `PhysicsAssetFactory` exposes no mesh
* property to Python, so the asset cannot be generated from a commandlet. The body does
* the job with `FPhysicsAssetUtils::CreateFromSkeletalMesh`, which is the same code path
* the editor's "Create Physics Asset" action uses.
*
* Editor only: it is compiled into the editor target and never shipped.
*/
UCLASS()
class GIGLINE_API UGigLineVehicleBuilder : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Build (or rebuild) a physics asset for `Mesh` at `PackagePath`/`AssetName`.
*
* @return the created asset, or nullptr with the reason written to the log.
*/
UFUNCTION(BlueprintCallable, Category = "GigLine|Vehicle")
static UPhysicsAsset* BuildPhysicsAsset(USkeletalMesh* Mesh, const FString& PackagePath,
const FString& AssetName);
};