test(vehicle): it is a kinematic teleport, not a force - and that is why every force test was clean

Instrumented the movement component so the write is caught at its source rather than inferred from the
actor: a UGigLineVehicleMovement subclass is substituted for the stock component through
AWheeledVehiclePawn::VehicleMovementComponentName, and it logs CreateVehicle(), the first UpdateState and
the component tick.

  mover tick: Z 661  -> 1143 -> 1443 -> 1718 ..., output null, wheels 0
  physics OFF at 3 s: Z 2247, and it does not move for a whole second afterwards
  profile: t=0.25 Z=515 vZ=-8 | t=0.75 Z=920 vZ=-22 | t=1.25 Z=1287 vZ=173

Two conclusions:

1. The mesh's physics state IS what moves the actor - switching physics off freezes it at once.
2. It moves WITHOUT VELOCITY: position rises ~700 cm/s while vZ oscillates around zero. That is a
   kinematic teleport, not an impulse and not a force - which is exactly why gravity, mass and
   penetration all measured clean. The vehicle-state output is null throughout, so the teleport comes
   from the Chaos layer driving the body kinematically while the vehicle itself was never created.

Pawn parked, captures regenerated so the demo stays clean.
This commit is contained in:
zwitschi committed 2026-11-09 21:51:52 +01:00
1 parent 348ed1ac69
commit d1636f6986
110 files changed
+263 -134

No files matched your search

+51 -3
View File
@@ -1,5 +1,8 @@
#include "GigLineVehicle.h"
#include "GigLineVehicleMovement.h"
#include "WheeledVehiclePawn.h"
#include "ChaosVehicleWheel.h"
#include "ChaosWheeledVehicleMovementComponent.h"
#include "Components/SkeletalMeshComponent.h"
@@ -36,7 +39,11 @@ UGigLineRearWheel::UGigLineRearWheel()
SuspensionMaxDrop = 8.0f;
}
AGigLineVehicle::AGigLineVehicle()
AGigLineVehicle::AGigLineVehicle(const FObjectInitializer& ObjectInitializer)
// Substitute the movement component class, which is the only place the Chaos rig can be
// instrumented: AWheeledVehiclePawn creates it, and the base name is how you replace it.
: Super(ObjectInitializer.SetDefaultSubobjectClass<UGigLineVehicleMovement>(
AWheeledVehiclePawn::VehicleMovementComponentName))
{
PrimaryActorTick.bCanEverTick = true;
@@ -332,6 +339,40 @@ void AGigLineVehicle::Tick(float DeltaSeconds)
}
}
// Velocity profile: an impulse shows as a spike at t=0, a force as steady growth.
{
static float NextSample = 0.0f;
if (SettleElapsed > NextSample && SettleElapsed < 2.5f)
{
NextSample += 0.25f;
if (USkeletalMeshComponent* Body = GetMesh())
{
const FVector V = Body->GetPhysicsLinearVelocity();
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle profile t=%.2f Z=%.0f vZ=%.0f vY=%.0f"),
SettleElapsed, GetActorLocation().Z, V.Z, V.Y);
}
}
}
// Decisive test: if the pawn keeps rising with physics switched OFF, physics is not the mover.
if (!bPhysicsOff && SettleElapsed > 3.0f)
{
bPhysicsOff = true;
if (USkeletalMeshComponent* Body = GetMesh())
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: physics OFF at Z %.0f (component velocity %.0f)"),
GetActorLocation().Z, Body->GetComponentVelocity().Z);
Body->SetSimulatePhysics(false);
}
}
if (bPhysicsOff && !bPhysicsOffLogged && SettleElapsed > 4.0f)
{
bPhysicsOffLogged = true;
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: after 1 s without physics, Z %.0f"),
GetActorLocation().Z);
}
// If the Chaos vehicle never came into existence (no wheels), its movement component has no
// valid state to drive, and continuing to tick it writes whatever is in that empty state back
// onto the actor - which is the only explanation that fits a still body and a moving actor.
@@ -377,6 +418,12 @@ void AGigLineVehicle::Tick(float DeltaSeconds)
B.Origin.X, B.Origin.Y, B.Origin.Z, B.BoxExtent.X, B.BoxExtent.Y, B.BoxExtent.Z);
}
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle geometry: %s | %s"), *BoxInfo, *BoundsInfo);
if (const UChaosVehicleMovementComponent* Mover = GetVehicleMovementComponent())
{
const auto& Output = const_cast<UChaosVehicleMovementComponent*>(Mover)->PhysicsVehicleOutput();
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle output: %s"),
Output.IsValid() ? TEXT("present") : TEXT("null"));
}
if (Body)
{
const FVector Vel = Body->GetPhysicsLinearVelocity();
@@ -424,13 +471,14 @@ void AGigLineVehicle::Tick(float DeltaSeconds)
}
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle diag: mesh=%d simulating=%d physasset=%s setups=%d wheels=%d "
"loc=%.0f,%.0f,%.0f"),
"loc=%.0f,%.0f,%.0f scale=%.2f mass=%.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);
GetActorLocation().X, GetActorLocation().Y, GetActorLocation().Z,
GetActorScale3D().Z, Body ? Body->GetMass() : -1.0f);
}
}
+3 -1
View File
@@ -50,7 +50,7 @@ class GIGLINE_API AGigLineVehicle : public AWheeledVehiclePawn
GENERATED_BODY()
public:
AGigLineVehicle();
AGigLineVehicle(const FObjectInitializer& ObjectInitializer);
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
@@ -84,5 +84,7 @@ private:
float SettleElapsed = 0.f;
bool bMassSet = false;
bool bMoverStopped = false;
bool bPhysicsOff = false;
bool bPhysicsOffLogged = false;
bool bSettleReported = false;
};
@@ -0,0 +1,45 @@
#include "GigLineVehicleMovement.h"
UGigLineVehicleMovement::UGigLineVehicleMovement()
{
// The async physics step is what the transform is written from; keep it on so the failure is
// reproduced rather than hidden.
PrimaryComponentTick.bCanEverTick = true;
}
void UGigLineVehicleMovement::CreateVehicle()
{
UE_LOG(LogTemp, Warning,
TEXT("GigLine mover: CreateVehicle() entered - UpdatedComponent %s"),
UpdatedComponent ? *UpdatedComponent->GetName() : TEXT("none"));
Super::CreateVehicle();
UE_LOG(LogTemp, Warning, TEXT("GigLine mover: CreateVehicle() returned - wheels %d"),
GetNumWheels());
}
void UGigLineVehicleMovement::UpdateState(float DeltaTime)
{
if (!bLoggedFirstFrame)
{
bLoggedFirstFrame = true;
UE_LOG(LogTemp, Warning, TEXT("GigLine mover: first UpdateState - output %s"),
PhysicsVehicleOutput().IsValid() ? TEXT("present") : TEXT("null"));
}
Super::UpdateState(DeltaTime);
}
void UGigLineVehicleMovement::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
Elapsed += DeltaTime;
if (Elapsed - LastLog > 0.5f)
{
LastLog = Elapsed;
UE_LOG(LogTemp, Warning,
TEXT("GigLine mover tick: Z %.0f, output %s, wheels %d"),
UpdatedComponent ? UpdatedComponent->GetComponentLocation().Z : -9999.0,
PhysicsVehicleOutput().IsValid() ? TEXT("present") : TEXT("null"),
GetNumWheels());
}
}
@@ -0,0 +1,34 @@
// Chaos vehicle movement with the one log line the broken rig needed (0.3.0).
//
// The vehicle pawn climbs 400-1400 cm/s with zero wheels while its body's reported velocity is ~0,
// and the transform does not change across APawn::Tick. The only writer left is this component, which
// runs in its own tick group and drives the transform from the async physics output. This subclass
// logs the two places that can write it, so the failure is caught at its source instead of inferred
// from where the actor ends up.
#pragma once
#include "CoreMinimal.h"
#include "ChaosWheeledVehicleMovementComponent.h"
#include "GigLineVehicleMovement.generated.h"
UCLASS(ClassGroup = (GigLine), meta = (BlueprintSpawnableComponent))
class GIGLINE_API UGigLineVehicleMovement : public UChaosWheeledVehicleMovementComponent
{
GENERATED_BODY()
public:
UGigLineVehicleMovement();
protected:
virtual void CreateVehicle() override;
virtual void UpdateState(float DeltaTime) override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
private:
/** Seconds since the component began ticking, used only to throttle the logging. */
float Elapsed = 0.0f;
float LastLog = 0.0f;
bool bLoggedFirstFrame = false;
};