Files
UnrealEngine/Engine/Source/Programs/HeadlessChaos/Private/HeadlessChaosTestModularVehicles.cpp
2025-05-18 13:04:45 +08:00

1096 lines
35 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "HeadlessChaos.h"
#include "HeadlessChaosTestUtility.h"
// for Module Unit Tests
#include "SimModule/ChassisModule.h"
#include "SimModule/AerofoilModule.h"
#include "SimModule/EngineModule.h"
#include "SimModule/ClutchModule.h"
#include "SimModule/TransmissionModule.h"
#include "SimModule/WheelModule.h"
#include "SimModule/SimModuleTree.h"
#include "SimModule/ModuleInput.h"
// for Simulation Tests
#include "Chaos/PBDRigidsEvolutionGBF.h"
#include "Chaos/Box.h"
#include "Chaos/Sphere.h"
#include "Chaos/Utilities.h"
//////////////////////////////////////////////////////////////////////////
// These tests are mostly working in real word units rather than Unreal
// units as it's easier to tell if the simulations are working close to
// reality. i.e. Google stopping distance @ 30MPH ==> typically 15 metres
//////////////////////////////////////////////////////////////////////////
namespace ChaosTest
{
using namespace Chaos;
struct FInputsContainer
{
FInputsContainer(Chaos::FAllInputs& InInputs)
{
ConfigureControlInputs(InInputs);
}
void ConfigureControlInputs(Chaos::FAllInputs& Inputs)
{
FModuleInputSetup InputData1(TEXT("Throttle"), EModuleInputValueType::MAxis1D);
InputSetupData.Add(InputData1);
FModuleInputSetup InputData2(TEXT("Brake"), EModuleInputValueType::MAxis1D);
InputSetupData.Add(InputData2);
FModuleInputSetup InputData3(TEXT("Steering"), EModuleInputValueType::MAxis1D);
InputSetupData.Add(InputData3);
FModuleInputSetup InputData4(TEXT("Clutch"), EModuleInputValueType::MAxis1D);
InputSetupData.Add(InputData4);
FModuleInputSetup InputData5(TEXT("Handbrake"), EModuleInputValueType::MAxis1D);
InputSetupData.Add(InputData5);
FModuleInputSetup InputData6(TEXT("Pitch"), EModuleInputValueType::MAxis1D);
InputSetupData.Add(InputData6);
FModuleInputSetup InputData7(TEXT("Yaw"), EModuleInputValueType::MAxis1D);
InputSetupData.Add(InputData7);
FModuleInputSetup InputData8(TEXT("Roll"), EModuleInputValueType::MAxis1D);
InputSetupData.Add(InputData8);
FModuleInputSetup InputData9(TEXT("ChangeUp"), EModuleInputValueType::MBoolean);
InputSetupData.Add(InputData9);
FModuleInputSetup InputData10(TEXT("ChangeDown"), EModuleInputValueType::MBoolean);
InputSetupData.Add(InputData10);
ValueContainer.Initialize(InputSetupData, NameMap);
ControlInputs = MakeUnique<FInputInterface>(NameMap, ValueContainer, EModuleInputQuantizationType::Default_16Bits);
Inputs.ControlInputs = ControlInputs.Get();
}
FInputInterface::FInputNameMap NameMap;
FModuleInputContainer ValueContainer;
TArray<FModuleInputSetup> InputSetupData;
TUniquePtr<FInputInterface> ControlInputs;
};
GTEST_TEST(AllTraits, ModularVehicleTest_Aerofoil)
{
FAerofoilSettings RWingSetup;
RWingSetup.Offset.Set(-0.8f, 3.0f, 0.0f);
RWingSetup.ForceAxis.Set(0.0f, 0.f, 1.0f);
RWingSetup.ControlRotationAxis.Set(0.f, 1.f, 0.f);
RWingSetup.Area = 8.2f;
RWingSetup.Camber = 3.0f;
RWingSetup.MaxControlAngle = 1.0f;
RWingSetup.StallAngle = 16.0f;
RWingSetup.Type = EAerofoil::Wing;
FAerofoilSimModule RWing(RWingSetup);
RWing.SetControlSurface(0.0f);
RWing.SetDensityOfMedium(RealWorldConsts::AirDensity());
float Altitude = 100.0f;
float DeltaTime = 1.0f / 30.0f;
//////////////////////////////////////////////////////////////////////////
FVector Velocity(1.0f, 0.0f, 0.0f);
float AOAFlat = RWing.CalcAngleOfAttackDegrees(FVector(0, 0, 1), FVector(-1, 0, 0));
EXPECT_LT(AOAFlat, SMALL_NUMBER);
float AOAFlat2 = RWing.CalcAngleOfAttackDegrees(FVector(0, 0, 1), FVector(1, 0, 0));
EXPECT_LT(AOAFlat2, SMALL_NUMBER);
float AOA90 = RWing.CalcAngleOfAttackDegrees(FVector(0, 0, 1), FVector(0, 0, 1));
EXPECT_LT(AOA90 - 90.0f, SMALL_NUMBER);
float AOA45 = RWing.CalcAngleOfAttackDegrees(FVector(0, 0, 1), FVector(0, 0.707, 0.707));
EXPECT_LT(AOA45 - 45.0f, SMALL_NUMBER);
//////////////////////////////////////////////////////////////////////////
// Lift
{
float Zero = RWing.CalcLiftCoefficient(0, 0);
EXPECT_LT(Zero, SMALL_NUMBER);
float Two = RWing.CalcLiftCoefficient(2, 0);
float NegTwo = RWing.CalcLiftCoefficient(-2, 0);
EXPECT_GT(Two, SMALL_NUMBER);
EXPECT_LT(NegTwo, SMALL_NUMBER);
EXPECT_LT(Two - FMath::Abs(NegTwo), SMALL_NUMBER);
float Three = RWing.CalcLiftCoefficient(0, 3);
float NegThree = RWing.CalcLiftCoefficient(0, -3);
EXPECT_GT(Three, SMALL_NUMBER);
EXPECT_LT(NegThree, SMALL_NUMBER);
EXPECT_LT(Three - FMath::Abs(NegThree), SMALL_NUMBER);
float Nine = RWing.CalcLiftCoefficient(6, 3);
float NegNine = RWing.CalcLiftCoefficient(-6, -3);
EXPECT_GT(Nine, SMALL_NUMBER);
EXPECT_LT(NegNine, SMALL_NUMBER);
EXPECT_LT(Nine - FMath::Abs(NegNine), SMALL_NUMBER);
float Stall = RWing.CalcLiftCoefficient(RWingSetup.StallAngle, 0);
float StallPlus = RWing.CalcLiftCoefficient(RWingSetup.StallAngle, 5);
EXPECT_GT(Stall, Nine);
EXPECT_GT(Stall, Three);
EXPECT_GT(Stall, Two);
EXPECT_GT(Stall, StallPlus);
}
// Drag
{
float Two = RWing.CalcDragCoefficient(2, 0);
float NegTwo = RWing.CalcDragCoefficient(-2, 0);
EXPECT_GT(Two, SMALL_NUMBER);
EXPECT_GT(NegTwo, SMALL_NUMBER);
EXPECT_LT(Two - NegTwo, SMALL_NUMBER);
float Six = RWing.CalcDragCoefficient(4, 2);
float NegSix = RWing.CalcDragCoefficient(-4, -2);
EXPECT_GT(Six, SMALL_NUMBER);
EXPECT_GT(NegSix, SMALL_NUMBER);
EXPECT_LT(Six - NegSix, SMALL_NUMBER);
float AltNegTwo = RWing.CalcDragCoefficient(2, -4);
EXPECT_GT(AltNegTwo, SMALL_NUMBER);
EXPECT_LT(AltNegTwo - NegTwo, SMALL_NUMBER);
}
////////////////////////////////////////////////////////////////////////////
FVector Velocity1(0.0f, 0.0f, 10.0f);
FVector RWForceZero = RWing.GetForce(Velocity1, Altitude, DeltaTime);
EXPECT_LT(FMath::Abs(RWForceZero.X), SMALL_NUMBER);
EXPECT_LT(FMath::Abs(RWForceZero.Y), SMALL_NUMBER);
EXPECT_LT(RWForceZero.Z, 0.f); // drag value opposes velocity direction
FVector Velocity2(0.0f, 10.0f, 10.0f);
FVector RWForce3 = RWing.GetForce(Velocity2, Altitude, DeltaTime);
EXPECT_LT(FMath::Abs(RWForce3.X), SMALL_NUMBER);
EXPECT_LT(RWForce3.Y, 0.0f);
EXPECT_LT(RWForce3.Z, 0.0f);
FVector Velocity3(10.0f, 0.0f, 0.0f);
FVector RWForce4 = RWing.GetForce(Velocity3, Altitude, DeltaTime);
EXPECT_LT(RWForce4.X, 0.0f);
EXPECT_LT(FMath::Abs(RWForce4.Y), SMALL_NUMBER);
EXPECT_GT(RWForce4.Z, 0.0f);
}
// Transmission
class FTransmissionTestClass : public FTransmissionSimModule
{
public:
FTransmissionTestClass(const FTransmissionSettings& Settings)
: FTransmissionSimModule(Settings)
{}
void Test_TransmissionManualGearSelection()
{
FAllInputs Inputs;
FInputsContainer IC(Inputs);
FSimModuleTree Tree;
EXPECT_EQ(GetCurrentGear(), 1);
// Immediate Gear Change, since Setup.GearChangeTime = 0.0f
ChangeUp();
EXPECT_EQ(GetCurrentGear(), 2);
ChangeUp();
ChangeUp();
ChangeUp();
EXPECT_EQ(GetCurrentGear(), 5);
ChangeUp();
EXPECT_EQ(GetCurrentGear(), 5);
SetGear(1);
EXPECT_EQ(GetCurrentGear(), 1);
ChangeDown();
EXPECT_EQ(GetCurrentGear(), 0);
ChangeDown();
EXPECT_EQ(GetCurrentGear(), -1);
ChangeDown();
EXPECT_EQ(GetCurrentGear(), -2);
ChangeDown();
EXPECT_EQ(GetCurrentGear(), -2);
ChangeUp();
EXPECT_EQ(GetCurrentGear(), -1);
ChangeUp();
EXPECT_EQ(GetCurrentGear(), 0);
SetGear(1);
// Now change settings so we have a delay in the gear changing
AccessSetup().GearChangeTime = 0.5f;
ChangeUp();
EXPECT_EQ(GetCurrentGear(), 0);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 0);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 2);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 2);
SetGear(4);
EXPECT_EQ(GetCurrentGear(), 0);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 0);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 4);
}
void Test_TransmissionAutoGearSelection()
{
FAllInputs Inputs;
FInputsContainer IC(Inputs);
FSimModuleTree Tree;
SetGear(1, true);
SetRPM(1400);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 1);
SetRPM(2000);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 1);
SetRPM(3000);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 2);
SetRPM(2000);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 2);
SetRPM(1000);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 1);
// stays in first, doesn't change to neutral
SetRPM(1000);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), 1);
SetGear(-2, true);
SetRPM(3000);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), -2);
SetRPM(1000);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), -1);
// stays in reverse first, doesn't change to neutral
SetRPM(1000);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), -1);
// changes to next reverse gear
SetRPM(3000);
Simulate(0.25f, Inputs, Tree);
EXPECT_EQ(GetCurrentGear(), -2);
}
void Test_TransmissionGearRatios()
{
float Ratio = 0;
Ratio = GetGearRatio(-1);
EXPECT_LT(-12.f - Ratio, SMALL_NUMBER); // -ve output for reverse gears
Ratio = GetGearRatio(0);
EXPECT_LT(Ratio, SMALL_NUMBER);
Ratio = GetGearRatio(1);
EXPECT_LT(16.f - Ratio, SMALL_NUMBER);
Ratio = GetGearRatio(2);
EXPECT_LT(12.f - Ratio, SMALL_NUMBER);
Ratio = GetGearRatio(3);
EXPECT_LT(8.f - Ratio, SMALL_NUMBER);
Ratio = GetGearRatio(4);
EXPECT_LT(4.f - Ratio, SMALL_NUMBER);
}
};
GTEST_TEST(AllTraits, ModularVehicleTest_TransmissionManualGearSelection)
{
// done this way so we can access protected function calls
FTransmissionSettings Setup;
{
Setup.ForwardRatios.Empty();
Setup.ReverseRatios.Empty();
Setup.ForwardRatios.Add(4.f);
Setup.ForwardRatios.Add(3.f);
Setup.ForwardRatios.Add(2.f);
Setup.ForwardRatios.Add(1.f);
Setup.ForwardRatios.Add(0.8f);
Setup.ReverseRatios.Add(3.f);
Setup.ReverseRatios.Add(6.f);
Setup.FinalDriveRatio = 4.f;
Setup.ChangeUpRPM = 3000;
Setup.ChangeDownRPM = 1200;
Setup.GearChangeTime = 0.0f;
Setup.TransmissionType = FTransmissionSettings::ETransType::ManualType;
Setup.AutoReverse = true;
Setup.TransmissionEfficiency = 1.0f;
}
FTransmissionTestClass Transmission(Setup);
Transmission.Test_TransmissionManualGearSelection();
}
GTEST_TEST(AllTraits, ModularVehicleTest_TransmissionAutoGearSelection)
{
FTransmissionSettings Setup;
{
Setup.ForwardRatios.Empty();
Setup.ReverseRatios.Empty();
Setup.ForwardRatios.Add(4.f);
Setup.ForwardRatios.Add(3.f);
Setup.ForwardRatios.Add(2.f);
Setup.ForwardRatios.Add(1.f);
Setup.ReverseRatios.Add(3.f);
Setup.ReverseRatios.Add(6.f);
Setup.FinalDriveRatio = 4.f;
Setup.ChangeUpRPM = 3000;
Setup.ChangeDownRPM = 1200;
Setup.GearChangeTime = 0.0f;
Setup.GearHysteresisTime = 0.0f;
Setup.TransmissionType = FTransmissionSettings::ETransType::AutomaticType;
Setup.AutoReverse = false;
Setup.TransmissionEfficiency = 1.0f;
}
FTransmissionTestClass Transmission(Setup);
Transmission.Test_TransmissionAutoGearSelection();
}
GTEST_TEST(AllTraits, ModularVehicleTest_TransmissionGearRatios)
{
FTransmissionSettings Setup;
{
Setup.ForwardRatios.Empty();
Setup.ReverseRatios.Empty();
Setup.ForwardRatios.Add(4.f);
Setup.ForwardRatios.Add(3.f);
Setup.ForwardRatios.Add(2.f);
Setup.ForwardRatios.Add(1.f);
Setup.ReverseRatios.Add(3.f);
Setup.FinalDriveRatio = 4.f;
Setup.ChangeUpRPM = 3000;
Setup.ChangeDownRPM = 1200;
Setup.GearChangeTime = 0.0f;
Setup.TransmissionType = FTransmissionSettings::ETransType::AutomaticType;
Setup.AutoReverse = true;
Setup.TransmissionEfficiency = 1.0f;
}
FTransmissionTestClass Transmission(Setup);
Transmission.Test_TransmissionGearRatios();
}
// Wheel
void SimulateBraking(FWheelSimModule& Wheel
, const float Gravity
, float VehicleSpeed
, float DeltaTime
, float& StoppingDistanceOut
, float& SimulationTimeOut
, bool bLoggingEnabled = false
)
{
FAllInputs Inputs;
FInputsContainer IC(Inputs);
Inputs.GetControls().SetValue(TEXT("Throttle"), 0.0f);
Inputs.GetControls().SetValue(TEXT("Brake"), 1.0f); // Apply full brake
FSimModuleTree Tree;
StoppingDistanceOut = 0.f;
SimulationTimeOut = 0.f;
float MaxSimTime = 15.0f;
float VehicleMass = 1300.f;
float VehicleMassPerWheel = 1300.f / 4.f;
Wheel.SetForceIntoSurface(VehicleMassPerWheel * Gravity);
// Road speed
FVector Velocity = FVector(VehicleSpeed, 0.f, 0.f);
// wheel rolling speed matches road speed
Wheel.SetLinearSpeed(Velocity.X);
if (bLoggingEnabled)
{
UE_LOG(LogChaos, Warning, TEXT("--------------------START---------------------"));
}
while (SimulationTimeOut < MaxSimTime)
{
// rolling speed matches road speed
Wheel.SetLocalLinearVelocity(Velocity);
Wheel.Simulate(DeltaTime, Inputs, Tree);
// deceleration from brake, F = m * a, a = F / m, v = dt * F / m
Velocity += DeltaTime * Wheel.GetForceFromFriction() / VehicleMassPerWheel;
StoppingDistanceOut += Velocity.X * DeltaTime;
if (bLoggingEnabled)
{
UE_LOG(LogChaos, Warning, TEXT("Wheel.GetForceFromFriction() %s"), *Wheel.GetForceFromFriction().ToString());
}
if (FMath::Abs(Velocity.X) < 0.05f)
{
Velocity.X = 0.f;
break; // break out early if already stopped
}
SimulationTimeOut += DeltaTime;
}
if (bLoggingEnabled)
{
UE_LOG(LogChaos, Warning, TEXT("---------------------END----------------------"));
}
}
void SimulateAccelerating(FWheelSimModule& Wheel
, const float Gravity
, const float DriveTorque
, float FinalVehicleSpeed
, float DeltaTime
, float& DistanceTravelledOut
, float& SimulationTimeOut
)
{
FAllInputs Inputs;
FInputsContainer IC(Inputs);
Inputs.GetControls().SetValue(TEXT("Throttle"), 1.0f); // Apply full throttle
Inputs.GetControls().SetValue(TEXT("Brake"), 0.0f);
FSimModuleTree Tree;
DistanceTravelledOut = 0.f;
SimulationTimeOut = 0.f;
float MaxSimTime = 15.0f;
float VehicleMass = 1300.f;
float VehicleMassPerWheel = VehicleMass / 4.f;
Wheel.SetForceIntoSurface(VehicleMassPerWheel * Gravity);
// Road speed
FVector Velocity = FVector(0.f, 0.f, 0.f);
// start from stationary
Wheel.SetLinearSpeed(Velocity.X);
while (SimulationTimeOut < MaxSimTime)
{
Wheel.SetLocalLinearVelocity(Velocity);
Wheel.SetDriveTorque(DriveTorque);
Wheel.Simulate(DeltaTime, Inputs, Tree);
Velocity += DeltaTime * Wheel.GetForceFromFriction() / VehicleMassPerWheel;
DistanceTravelledOut += Velocity.X * DeltaTime;
SimulationTimeOut += DeltaTime;
if (FMath::Abs(Velocity.X) >= FinalVehicleSpeed)
{
break; // time is up
}
}
}
GTEST_TEST(AllTraits, ModularVehicleTest_WheelBrakingLongitudinalSlip)
{
FWheelSettings Setup;
Setup.ABSEnabled = false;
Setup.TractionControlEnabled = false;
Setup.SteeringEnabled = true;
Setup.HandbrakeEnabled = true;
Setup.Radius = 0.3f;
Setup.FrictionMultiplier = 1.0f;
Setup.CorneringStiffness = 1000.0f;
Setup.LateralSlipGraphMultiplier = 0.7f;
FWheelSimModule Wheel(Setup);
// Google braking distance at 30mph says 14m (not interested in the thinking distance part)
// So using a range 10-20 to ensure we are in the correct ballpark.
// If specified more accurately in the test, then modifying the code would break the test all the time.
// units meters
float Gravity = 9.8f;
float StoppingDistanceTolerance = 0.5f;
float DeltaTime = 1.f / 30.f;
float StoppingDistanceA = 0.f;
float SimulationTime = 0.0f;
Wheel.SetSurfaceFriction(RealWorldConsts::DryRoadFriction());
// reasonably ideal stopping distance - traveling forwards
Wheel.AccessSetup().MaxBrakeTorque = 650.0f;
SimulateBraking(Wheel, Gravity, MPHToMS(30.f), DeltaTime, StoppingDistanceA, SimulationTime);
EXPECT_GT(StoppingDistanceA, 10.f);
EXPECT_LT(StoppingDistanceA, 20.f);
// traveling backwards stops just the same
float StoppingDistanceReverseDir = 0.f;
Wheel.AccessSetup().MaxBrakeTorque = 650.0f;
SimulateBraking(Wheel, Gravity, MPHToMS(-30.f), DeltaTime, StoppingDistanceReverseDir, SimulationTime);
EXPECT_GT(StoppingDistanceReverseDir, -20.f);
EXPECT_LT(StoppingDistanceReverseDir, -10.f);
EXPECT_LT(StoppingDistanceA - FMath::Abs(StoppingDistanceReverseDir), StoppingDistanceTolerance);
// Changing to units of Cm should yield the same results
float MToCm = 100.0f;
float StoppingDistanceCm = 0.f;
Wheel.AccessSetup().MaxBrakeTorque = 650.0f * MToCm * MToCm;
Wheel.AccessSetup().Radius = (0.3f * MToCm);
SimulateBraking(Wheel, Gravity * MToCm, MPHToCmS(30.f), DeltaTime, StoppingDistanceCm, SimulationTime);
EXPECT_NEAR(StoppingDistanceCm, StoppingDistanceA * MToCm, 1.0f);
// Similar results with different delta time
float StoppingDistanceDiffDT = 0.f;
Wheel.AccessSetup().MaxBrakeTorque = 650.0f;
Wheel.AccessSetup().Radius = 0.3f;
SimulateBraking(Wheel, Gravity, MPHToMS(30.f), DeltaTime * 0.25f, StoppingDistanceDiffDT, SimulationTime);
EXPECT_LT(StoppingDistanceA - StoppingDistanceDiffDT, StoppingDistanceTolerance);
// barely touching the brake - going to take longer to stop
float StoppingDistanceLightBraking = 0.f;
Wheel.AccessSetup().MaxBrakeTorque = 150.0f;
SimulateBraking(Wheel, Gravity, MPHToMS(30.f), DeltaTime, StoppingDistanceLightBraking, SimulationTime);
EXPECT_GT(StoppingDistanceLightBraking, StoppingDistanceA);
// locking the wheels / too much brake torque -> dynamic friction rather than static friction -> going to take longer to stop
float StoppingDistanceTooHeavyBreaking = 0.f;
Wheel.AccessSetup().MaxBrakeTorque = 5000.0f;
SimulateBraking(Wheel, Gravity, MPHToMS(30.f), DeltaTime, StoppingDistanceTooHeavyBreaking, SimulationTime);
EXPECT_GT(StoppingDistanceTooHeavyBreaking, StoppingDistanceA);
// Would have locked the wheels but ABS prevents skidding
Wheel.AccessSetup().ABSEnabled = true;
float StoppingDistanceTooHeavyBreakingABS = 0.f;
Wheel.AccessSetup().MaxBrakeTorque = 5000.0f;
SimulateBraking(Wheel, Gravity, MPHToMS(30.f), DeltaTime, StoppingDistanceTooHeavyBreakingABS, SimulationTime);
EXPECT_LT(StoppingDistanceTooHeavyBreakingABS, StoppingDistanceTooHeavyBreaking);
Wheel.AccessSetup().ABSEnabled = false;
// lower initial speed - stops more quickly
float StoppingDistanceLowerSpeed = 0.f;
Wheel.AccessSetup().MaxBrakeTorque = 650.0f;
SimulateBraking(Wheel, Gravity, MPHToMS(20.f), DeltaTime, StoppingDistanceLowerSpeed, SimulationTime);
EXPECT_LT(StoppingDistanceLowerSpeed, StoppingDistanceA);
// higher initial speed - stops more slowly
float StoppingDistanceHigherSpeed = 0.f;
Wheel.AccessSetup().MaxBrakeTorque = 650.0f;
SimulateBraking(Wheel, Gravity, MPHToMS(60.f), DeltaTime, StoppingDistanceHigherSpeed, SimulationTime);
EXPECT_GT(StoppingDistanceHigherSpeed, StoppingDistanceA);
// slippy surface - stops more slowly
float StoppingDistanceLowFriction = 0.f;
Wheel.SetSurfaceFriction(0.3f);
Wheel.AccessSetup().MaxBrakeTorque = 650.0f;
SimulateBraking(Wheel, Gravity, MPHToMS(30.f), DeltaTime, StoppingDistanceLowFriction, SimulationTime);
EXPECT_GT(StoppingDistanceLowFriction, StoppingDistanceA);
}
GTEST_TEST(AllTraits, ModularVehicleTest_WheelAcceleratingLongitudinalSlip)
{
FWheelSettings Setup;
Setup.ABSEnabled = false;
Setup.TractionControlEnabled = false;
Setup.SteeringEnabled = false;
Setup.HandbrakeEnabled = false;
Setup.Radius = 0.3f;
Setup.FrictionMultiplier = 1.0f;
Setup.CorneringStiffness = 1000.0f;
Setup.LateralSlipGraphMultiplier = 0.7f;
FWheelSimModule Wheel(Setup);
// There could be one frame extra computation on the acceleration since the last frame of brake is not using the full
// amount of torque, it's clearing the last remaining velocity without pushing the vehicle back in the opposite direction
// Hence a slightly larger tolerance for the result
float AccelerationResultsTolerance = 1.0f; // meters
// units meters
float Gravity = 9.8f;
float DeltaTime = 1.f / 30.f;
float DriveTorque = 0.0;
float StoppingDistanceA = 0.f;
float SimulationTimeBrake = 0.0f;
Wheel.SetSurfaceFriction(RealWorldConsts::DryRoadFriction());
// How far & what time does it take to stop from 30MPH to rest
Wheel.AccessSetup().MaxBrakeTorque = 650.0f;
SimulateBraking(Wheel, Gravity, MPHToMS(30.0f), DeltaTime, StoppingDistanceA, SimulationTimeBrake);
// How far and what time does it take to accelerate from rest to 30MPH
float SimulationTimeAccel = 0.0f;
float DrivingDistanceA = 0.f;
DriveTorque = 650.0f;
SimulateAccelerating(Wheel, Gravity, DriveTorque, MPHToMS(30.0f), DeltaTime, DrivingDistanceA, SimulationTimeAccel);
// 0-30 MPH and 30-0 MPH should be the same if there's no slipping and accel torque was same as the brake torque run
EXPECT_LT(DrivingDistanceA - StoppingDistanceA, AccelerationResultsTolerance);
EXPECT_LT(SimulationTimeAccel - SimulationTimeBrake, AccelerationResultsTolerance);
// same range as braking from 30MPH
EXPECT_GT(DrivingDistanceA, 10.f);
EXPECT_LT(DrivingDistanceA, 20.f);
// Unreal units cm - Note for the same results the radius needs to remain at 0.3m and not also be scaled to 30(cm)
float SimulationTimeAccelCM = 0.0f;
float MToCm = 100.0f;
float DrivingDistanceCM = 0.f;
DriveTorque = 650.0f * MToCm * MToCm;
Wheel.AccessSetup().Radius = (0.3f * MToCm);
SimulateAccelerating(Wheel, Gravity * MToCm, DriveTorque, MPHToCmS(30.0f), DeltaTime, DrivingDistanceCM, SimulationTimeAccelCM);
EXPECT_GT(DrivingDistanceCM, 10.f * MToCm);
EXPECT_LT(DrivingDistanceCM, 20.f * MToCm);
EXPECT_NEAR(DrivingDistanceCM, DrivingDistanceA * MToCm, AccelerationResultsTolerance);
float SimulationTimeAccelSpin = 0.0f;
float DrivingDistanceWheelspin = 0.f;
Wheel.AccessSetup().Radius = 0.3f;
DriveTorque = 5000; // definitely cause wheel spin
SimulateAccelerating(Wheel, Gravity, DriveTorque, 30.0f, DeltaTime, DrivingDistanceWheelspin, SimulationTimeAccelSpin);
// drives further to reach the same speed
EXPECT_GT(DrivingDistanceWheelspin, DrivingDistanceA);
// takes longer to reach the same speed
EXPECT_GT(SimulationTimeAccelSpin, SimulationTimeAccel);
// Enable traction control should be better than both of the above
float SimulationTimeAccelTC = 0.0f;
float DrivingDistanceTC = 0.f;
Wheel.AccessSetup().TractionControlEnabled = true;
DriveTorque = 5000; // definitely cause wheel spin
SimulateAccelerating(Wheel, Gravity, DriveTorque, MPHToMS(30.0f), DeltaTime, DrivingDistanceTC, SimulationTimeAccelTC);
// reaches target speed in a shorter distance
EXPECT_LT(DrivingDistanceTC, DrivingDistanceWheelspin);
// reaches speed quicker with TC on when wheel would be slipping from drive torque
EXPECT_LT(SimulationTimeAccelTC, SimulationTimeAccelSpin);
}
GTEST_TEST(AllTraits, ModularVehicleTest_WheelRolling)
{
FWheelSettings Setup;
Setup.Radius = 0.3f;
FWheelSimModule Wheel(Setup);
FAllInputs Inputs;
FInputsContainer IC(Inputs);
Inputs.GetControls().SetValue(TEXT("Throttle"), 0.0f);
Inputs.GetControls().SetValue(TEXT("Brake"), 0.0f);
FSimModuleTree Tree;
float DeltaTime = 1.f / 30.f;
float MaxSimTime = 10.0f;
float Tolerance = 0.1f; // wheel friction losses slow wheel speed
//------------------------------------------------------------------
// Car is moving FORWARDS - with AMPLE friction we would expect an initially
// static rolling wheel to speed up and match the vehicle speed
FVector VehicleGroundSpeed(10.0f, 0.0f, 0.0f);
Wheel.SetSurfaceFriction(1.0f); // Some wheel/ground friction
Wheel.SetForceIntoSurface(250.f); // wheel pressed into the ground, to give it grip
Wheel.SetAngularVelocity(0.f);
Wheel.SetLocalLinearVelocity(VehicleGroundSpeed);
// initially wheel is static
EXPECT_LT(Wheel.GetAngularVelocity(), SMALL_NUMBER);
// after some time, the wheel picks up speed to match the vehicle speed
float SimulatedTime = 0.f;
while (SimulatedTime < MaxSimTime)
{
Wheel.Simulate(DeltaTime, Inputs, Tree);
SimulatedTime += DeltaTime;
}
// there's enough grip to cause the wheel to spin and match the vehivle speed
float WheelGroundSpeed = Wheel.GetAngularVelocity() * Wheel.GetEffectiveRadius();
EXPECT_LT(VehicleGroundSpeed.X - WheelGroundSpeed, Tolerance);
EXPECT_LT(VehicleGroundSpeed.X - WheelGroundSpeed, Tolerance);
EXPECT_GT(Wheel.GetAngularVelocity(), 0.f); // +ve spin on it
//------------------------------------------------------------------
// Car is moving BACKWARDS - with AMPLE friction we would expect an initially
// static rolling wheel to speed up and match the vehicle speed
VehicleGroundSpeed.Set(-10.0f, 0.0f, 0.0f);
Wheel.SetSurfaceFriction(1.0f); // Some wheel/ground friction
Wheel.SetForceIntoSurface(250.f); // wheel pressed into the ground, to give it grip
Wheel.SetAngularVelocity(0.f);
Wheel.SetLocalLinearVelocity(VehicleGroundSpeed);
// initially wheel is static
EXPECT_LT(Wheel.GetAngularVelocity(), SMALL_NUMBER);
// after some time, the wheel picks up speed to match the vehicle speed
SimulatedTime = 0.f;
while (SimulatedTime < MaxSimTime)
{
Wheel.Simulate(DeltaTime, Inputs, Tree);
SimulatedTime += DeltaTime;
}
// there's enough grip to cause the wheel to spin and match the vehicle speed
WheelGroundSpeed = Wheel.GetAngularVelocity() * Wheel.GetEffectiveRadius();
EXPECT_LT(VehicleGroundSpeed.X - WheelGroundSpeed, Tolerance);
EXPECT_LT(VehicleGroundSpeed.X - Wheel.GetLinearSpeed(), Tolerance);
EXPECT_LT(Wheel.GetAngularVelocity(), 0.f); // -ve spin on it
//------------------------------------------------------------------
// Car is moving FORWARDS - with NO friction we would expect an initially
// static wheel to NOT speed up to match the vehicle speed
Wheel.SetSurfaceFriction(0.0f); // No wheel/ground friction
Wheel.SetForceIntoSurface(250.f); // wheel pressed into the ground, to give it grip
Wheel.SetAngularVelocity(0.f);
Wheel.SetLocalLinearVelocity(VehicleGroundSpeed);
// initially wheel is static
EXPECT_LT(Wheel.GetAngularVelocity(), SMALL_NUMBER);
// after some time, the wheel picks up speed to match the vehicle speed
SimulatedTime = 0.f;
while (SimulatedTime < MaxSimTime)
{
Wheel.Simulate(DeltaTime, Inputs, Tree);
SimulatedTime += DeltaTime;
}
WheelGroundSpeed = Wheel.GetAngularVelocity() * Wheel.GetEffectiveRadius();
// wheel is just sliding there's no friction to make it spin
EXPECT_LT(WheelGroundSpeed, SMALL_NUMBER);
}
// Engine
GTEST_TEST(AllTraits, ModularVehicleTest_EngineRPM)
{
FAllInputs Inputs;
FInputsContainer IC(Inputs);
FSimModuleTree Tree;
FEngineSettings Setup;
{
Setup.MaxRPM = 5000;
Setup.IdleRPM = 1000;
Setup.MaxTorque = 400.f;
Setup.EngineBrakeEffect = 200.0f;
Setup.EngineInertia = 100.0f;
Setup.TorqueCurve.Empty();
Setup.TorqueCurve.AddNormalized(0.5f);
Setup.TorqueCurve.AddNormalized(0.5f);
Setup.TorqueCurve.AddNormalized(0.5f);
Setup.TorqueCurve.AddNormalized(0.5f);
Setup.TorqueCurve.AddNormalized(0.6f);
Setup.TorqueCurve.AddNormalized(0.7f);
Setup.TorqueCurve.AddNormalized(0.8f);
Setup.TorqueCurve.AddNormalized(0.9f);
Setup.TorqueCurve.AddNormalized(1.0f);
Setup.TorqueCurve.AddNormalized(0.9f);
Setup.TorqueCurve.AddNormalized(0.7f);
Setup.TorqueCurve.AddNormalized(0.5f);
}
FEngineSimModule Engine(Setup);
Inputs.GetControls().SetValue(TEXT("Throttle"), 0.0f);
float DeltaTime = 1.0f / 30.0f;
float TOLERANCE = 0.1f;
// engine idle - no throttle
for (int i = 0; i < 200; i++)
{
Engine.Simulate(DeltaTime, Inputs, Tree);
}
EXPECT_LT(Engine.GetRPM() - Engine.Setup().IdleRPM, TOLERANCE);
// apply half throttle
Inputs.GetControls().SetValue(TEXT("Throttle"), 0.5f);
for (int i = 0; i < 100; i++)
{
Engine.Simulate(DeltaTime, Inputs, Tree);
//UE_LOG(LogChaos, Warning, TEXT("EngineSpeed %.2f rad/sec (%.1f RPM)"), Engine.GetAngularVelocity(), Engine.GetRPM());
}
EXPECT_GT(Engine.GetRPM(), Engine.Setup().IdleRPM);
Inputs.GetControls().SetValue(TEXT("Throttle"), 0.0f);
// engine idle - no throttle
for (int i = 0; i < 200; i++)
{
Engine.Simulate(DeltaTime, Inputs, Tree);
//UE_LOG(LogChaos, Warning, TEXT("EngineSpeed %.2f rad/sec (%.1f RPM)"), Engine.GetAngularVelocity(), Engine.GetRPM());
}
EXPECT_LT(Engine.GetRPM() - Engine.Setup().IdleRPM, TOLERANCE);
}
GTEST_TEST(AllTraits, ModularVehicleTest_SimModuleTree_EngineDrivingWheelsThroughClutch)
{
FAllInputs Inputs;
FInputsContainer IC(Inputs);
FSimModuleTree Tree;
float TOLERANCE = 0.1f;
FEngineSettings Setup;
{
Setup.MaxRPM = 5000;
Setup.IdleRPM = 1000;
Setup.MaxTorque = 400.f;
Setup.EngineBrakeEffect = 20.0f;
Setup.EngineInertia = 100.0f;
Setup.TorqueCurve.Empty();
Setup.TorqueCurve.AddNormalized(0.5f);
Setup.TorqueCurve.AddNormalized(0.5f);
Setup.TorqueCurve.AddNormalized(0.5f);
Setup.TorqueCurve.AddNormalized(0.5f);
Setup.TorqueCurve.AddNormalized(0.6f);
Setup.TorqueCurve.AddNormalized(0.7f);
Setup.TorqueCurve.AddNormalized(0.8f);
Setup.TorqueCurve.AddNormalized(0.9f);
Setup.TorqueCurve.AddNormalized(1.0f);
Setup.TorqueCurve.AddNormalized(0.9f);
Setup.TorqueCurve.AddNormalized(0.7f);
Setup.TorqueCurve.AddNormalized(0.5f);
}
FChassisSettings ChassisSettings;
int RootNodeIndex = Tree.AddRoot(new FChassisSimModule(ChassisSettings));
FEngineSettings EngineSettings;
int NodeIndex = Tree.AddNodeBelow(RootNodeIndex, new FEngineSimModule(EngineSettings));
FClutchSettings ClutchSettings;
NodeIndex = Tree.AddNodeBelow(NodeIndex, new FClutchSimModule(ClutchSettings));
FTransmissionSettings TransmissionSettings;
int TransmissionNodeIndex = Tree.AddNodeBelow(NodeIndex, new FTransmissionSimModule(TransmissionSettings));
FWheelSettings WheelSettingsDriven;
FWheelSettings WheelSettingsRolling;
int Wheel0Idx = Tree.AddNodeBelow(TransmissionNodeIndex, new FWheelSimModule(WheelSettingsDriven));
int Wheel1Idx = Tree.AddNodeBelow(TransmissionNodeIndex, new FWheelSimModule(WheelSettingsDriven));
int Wheel2Idx = Tree.AddNodeBelow(RootNodeIndex, new FWheelSimModule(WheelSettingsRolling));
int Wheel3Idx = Tree.AddNodeBelow(RootNodeIndex, new FWheelSimModule(WheelSettingsRolling));
const FWheelSimModule& Wheel0 = *static_cast<const FWheelSimModule*>(Tree.GetSimModule(Wheel0Idx));
const FWheelSimModule& Wheel1 = *static_cast<const FWheelSimModule*>(Tree.GetSimModule(Wheel1Idx));
const FWheelSimModule& Wheel2 = *static_cast<const FWheelSimModule*>(Tree.GetSimModule(Wheel2Idx));
const FWheelSimModule& Wheel3 = *static_cast<const FWheelSimModule*>(Tree.GetSimModule(Wheel3Idx));
IPhysicsProxyBase* Proxy = nullptr;
FPBDRigidParticleHandle* Particle = nullptr;
float DeltaTime = 1.0f / 30.0f;
// We need to setup reverse pointer to tree
for (int I = 0; I < Tree.GetNumNodes(); I++)
{
Tree.AccessSimModule(I)->SetSimModuleTree(&Tree);
}
// Throttle ON
Inputs.GetControls().SetValue(TEXT("Throttle"), 1.0f);
Inputs.GetControls().SetValue(TEXT("Brake"), 0.0f);
Inputs.GetControls().SetValue(TEXT("Clutch"), 0.0f);
// wheels not moving initially
EXPECT_NEAR(Wheel0.GetRPM(), 0, TOLERANCE);
EXPECT_NEAR(Wheel1.GetRPM(), 0, TOLERANCE);
EXPECT_NEAR(Wheel2.GetRPM(), 0, TOLERANCE);
EXPECT_NEAR(Wheel3.GetRPM(), 0, TOLERANCE);
// Simulate
for (int I=0; I<50; I++)
{
Tree.Simulate(DeltaTime, Inputs, Proxy, Particle);
}
// driven wheels are turning
EXPECT_GT(Wheel0.GetRPM(), TOLERANCE);
EXPECT_GT(Wheel1.GetRPM(), TOLERANCE);
// non-driven wheels are still static
EXPECT_NEAR(Wheel2.GetRPM(), 0, TOLERANCE);
EXPECT_NEAR(Wheel3.GetRPM(), 0, TOLERANCE);
// Brake ON
Inputs.GetControls().SetValue(TEXT("Throttle"), 0.0f);
Inputs.GetControls().SetValue(TEXT("Brake"), 1.0f);
Inputs.GetControls().SetValue(TEXT("Clutch"), 0.0f);
// Simulate
for (int I = 0; I < 50; I++)
{
Tree.Simulate(DeltaTime, Inputs, Proxy, Particle);
}
// all wheels stop spinning
EXPECT_NEAR(Wheel0.GetRPM(), 0, TOLERANCE);
EXPECT_NEAR(Wheel1.GetRPM(), 0, TOLERANCE);
EXPECT_NEAR(Wheel2.GetRPM(), 0, TOLERANCE);
EXPECT_NEAR(Wheel3.GetRPM(), 0, TOLERANCE);
// Throttle ON, Clutch depressed
Inputs.GetControls().SetValue(TEXT("Throttle"), 1.0f);
Inputs.GetControls().SetValue(TEXT("Brake"), 0.0f);
Inputs.GetControls().SetValue(TEXT("Clutch"), 1.0f);
// Simulate
for (int I = 0; I < 50; I++)
{
Tree.Simulate(DeltaTime, Inputs, Proxy, Particle);
}
// driven wheels are not turning due to disengaged clutch
EXPECT_NEAR(Wheel0.GetRPM(), 0, TOLERANCE);
EXPECT_NEAR(Wheel1.GetRPM(), 0, TOLERANCE);
}
GTEST_TEST(AllTraits, ModularVehicleTest_SimModuleTree_WheelsSpinEngineShouldSpin)
{
FAllInputs Inputs;
FInputsContainer IC(Inputs);
FSimModuleTree Tree;
float TOLERANCE = 0.1f;
FChassisSettings ChassisSettings;
int RootNodeIndex = Tree.AddRoot(new FChassisSimModule(ChassisSettings));
Inputs.GetControls().SetValue(TEXT("Throttle"), 0.0f);
Inputs.GetControls().SetValue(TEXT("Brake"), 0.0f);
FEngineSettings EngineSettings;
{
EngineSettings.MaxRPM = 5000;
EngineSettings.IdleRPM = 0;
EngineSettings.MaxTorque = 400.f;
EngineSettings.EngineBrakeEffect = 0.0f;
EngineSettings.EngineInertia = 100.0f;
EngineSettings.TorqueCurve.Empty();
EngineSettings.TorqueCurve.AddNormalized(0.5f);
EngineSettings.TorqueCurve.AddNormalized(0.5f);
EngineSettings.TorqueCurve.AddNormalized(0.5f);
EngineSettings.TorqueCurve.AddNormalized(0.5f);
EngineSettings.TorqueCurve.AddNormalized(0.6f);
EngineSettings.TorqueCurve.AddNormalized(0.7f);
EngineSettings.TorqueCurve.AddNormalized(0.8f);
EngineSettings.TorqueCurve.AddNormalized(0.9f);
EngineSettings.TorqueCurve.AddNormalized(1.0f);
EngineSettings.TorqueCurve.AddNormalized(0.9f);
EngineSettings.TorqueCurve.AddNormalized(0.7f);
EngineSettings.TorqueCurve.AddNormalized(0.5f);
}
int EngineNodeIndex = Tree.AddNodeBelow(RootNodeIndex, new FEngineSimModule(EngineSettings));
FWheelSettings WheelSettingsDriven;
FWheelSettings WheelSettingsRolling;
WheelSettingsDriven.AutoHandbrakeEnabled = false;
WheelSettingsRolling.AutoHandbrakeEnabled = false;
int Wheel0Idx = Tree.AddNodeBelow(EngineNodeIndex, new FWheelSimModule(WheelSettingsDriven));
int Wheel1Idx = Tree.AddNodeBelow(EngineNodeIndex, new FWheelSimModule(WheelSettingsDriven));
FEngineSimModule& Engine = *static_cast<FEngineSimModule*>(Tree.AccessSimModule(EngineNodeIndex));
FWheelSimModule& Wheel0 = *static_cast<FWheelSimModule*>(Tree.AccessSimModule(Wheel0Idx));
FWheelSimModule& Wheel1 = *static_cast<FWheelSimModule*>(Tree.AccessSimModule(Wheel1Idx));
IPhysicsProxyBase* Proxy = nullptr;
FPBDRigidParticleHandle* Particle = nullptr;
float DeltaTime = 1.0f / 30.0f;
// We need to setup reverse pointer to tree
for (int I = 0; I < Tree.GetNumNodes(); I++)
{
Tree.AccessSimModule(I)->SetSimModuleTree(&Tree);
}
// Simulate - engine takes speed from wheels spin
for (int I = 0; I < 5; I++)
{
Wheel0.SetLinearSpeed(100);
Wheel1.SetLinearSpeed(100);
Tree.Simulate(DeltaTime, Inputs, Proxy, Particle);
}
EXPECT_NEAR(Engine.GetRPM(), Wheel0.GetRPM(), TOLERANCE);
// Simulate - engine will just need to take average of connected wheels speeds
for (int I = 0; I < 5; I++)
{
Wheel0.SetLinearSpeed(300);
Wheel1.SetLinearSpeed(100);
Tree.Simulate(DeltaTime, Inputs, Proxy, Particle);
}
EXPECT_NEAR(Engine.GetRPM(), (Wheel0.GetRPM() + Wheel1.GetRPM())*0.5f, TOLERANCE);
}
}