prerelease

This commit is contained in:
2025-11-06 12:13:30 +07:00
parent 0905876b99
commit ae5cd2c8b6
48 changed files with 3753 additions and 1404 deletions

View File

@@ -1,49 +1,207 @@
using Sandbox;
using System;
using System;
using System.Collections.Generic;
using Sandbox;
namespace VeloX.Powertrain;
namespace VeloX;
public class Clutch : PowertrainComponent
public partial class Clutch : PowertrainComponent
{
[Property] public override float Inertia { get; set; } = 0.002f;
[Property] public float SlipTorque { get; set; } = 1000f;
protected override void OnAwake()
{
base.OnAwake();
Name ??= "Clutch";
}
public float Pressing { get; set; } = 1; // todo
/// <summary>
/// RPM at which automatic clutch will try to engage.
/// </summary>
[Property] public float EngagementRPM { get; set; } = 1200f;
public float ThrottleEngagementOffsetRPM = 400f;
/// <summary>
/// Clutch engagement in range [0,1] where 1 is fully engaged clutch.
/// Affected by Slip Torque field as the clutch can transfer [clutchEngagement * SlipTorque] Nm
/// meaning that higher value of SlipTorque will result in more sensitive clutch.
/// </summary>
[Range( 0, 1 ), Property] public float ClutchInput { get; set; }
/// <summary>
/// Curve representing pedal travel vs. clutch engagement. Should start at 0,0 and end at 1,1.
/// </summary>
[Property] public Curve EngagementCurve { get; set; } = new( new List<Curve.Frame>() { new( 0, 0 ), new( 1, 1 ) } );
public enum ClutchControlType
{
Automatic,
Manual
}
[Property] public ClutchControlType СontrolType { get; set; } = ClutchControlType.Automatic;
/// <summary>
/// The RPM range in which the clutch will go from disengaged to engaged and vice versa.
/// E.g. if set to 400 and engagementRPM is 1000, 1000 will mean clutch is fully disengaged and
/// 1400 fully engaged. Setting it too low might cause clutch to hunt/oscillate.
/// </summary>
[Property] public float EngagementRange { get; set; } = 400f;
/// <summary>
/// Torque at which the clutch will slip / maximum torque that the clutch can transfer.
/// This value also affects clutch engagement as higher slip value will result in clutch
/// that grabs higher up / sooner. Too high slip torque value combined with low inertia of
/// powertrain components might cause instability in powertrain solver.
/// </summary>
[Property] public float SlipTorque { get; set; } = 500f;
/// <summary>
/// Amount of torque that will be passed through clutch even when completely disengaged
/// to emulate torque converter creep on automatic transmissions.
/// Should be higher than rolling resistance of the wheels to get the vehicle rolling.
/// </summary>
[Range( 0, 100f ), Property] public float CreepTorque { get; set; } = 0;
[Property] public float CreepSpeedLimit { get; set; } = 1f;
/// <summary>
/// Clutch engagement based on ClutchInput and the clutchEngagementCurve
/// </summary>
[Property, ReadOnly]
public float Engagement => _clutchEngagement;
private float _clutchEngagement;
protected override void OnStart()
{
base.OnStart();
SlipTorque = Controller.Engine.EstimatedPeakTorque * 1.5f;
}
public override float QueryAngularVelocity( float angularVelocity, float dt )
{
InputAngularVelocity = angularVelocity;
// Return input angular velocity if InputNameHash or OutputNameHash is 0
if ( InputNameHash == 0 || OutputNameHash == 0 )
{
return InputAngularVelocity;
}
// Adjust clutch engagement based on conditions
if ( СontrolType == ClutchControlType.Automatic )
{
Engine engine = Controller.Engine;
// Override engagement when shifting to smoothly engage and disengage gears
if ( Controller.Transmission.IsShifting )
{
float shiftProgress = Controller.Transmission.ShiftProgress;
ClutchInput = MathF.Abs( MathF.Cos( MathF.PI * shiftProgress ) );
}
// Clutch engagement calculation for automatic clutch
else
{
// Calculate engagement
// Engage the clutch if the input spinning faster than the output, but also if vice versa.
float throttleInput = Controller.SwappedThrottle;
float finalEngagementRPM = EngagementRPM + ThrottleEngagementOffsetRPM * (throttleInput * throttleInput);
float referenceRPM = MathF.Max( InputRPM, OutputRPM );
ClutchInput = (referenceRPM - finalEngagementRPM) / EngagementRange;
ClutchInput = Math.Clamp( ClutchInput, 0f, 1f );
// Avoid disconnecting clutch at high speed
if ( engine.OutputRPM > engine.IdleRPM * 1.1f && Controller.TotalSpeed > 3f )
{
ClutchInput = 1f;
}
}
if ( Controller.IsClutching > 0 )
{
ClutchInput = 1 - Controller.IsClutching;
}
}
else if ( СontrolType == ClutchControlType.Manual )
{
// Manual clutch engagement through user input
ClutchInput = Controller.IsClutching;
}
OutputAngularVelocity = InputAngularVelocity * _clutchEngagement;
float Wout = Output.QueryAngularVelocity( OutputAngularVelocity, dt ) * _clutchEngagement;
float Win = angularVelocity * (1f - _clutchEngagement);
return Wout + Win;
}
public override float QueryInertia()
{
if ( !HasOutput )
if ( OutputNameHash == 0 )
{
return Inertia;
}
return Inertia + Output.QueryInertia() * Pressing;
}
public override float QueryAngularVelocity( float angularVelocity )
{
this.angularVelocity = angularVelocity;
if ( !HasOutput )
return angularVelocity;
float outputW = Output.QueryAngularVelocity( angularVelocity ) * Pressing;
float inputW = angularVelocity * (1 - Pressing);
return outputW + inputW;
float I = Inertia + Output.QueryInertia() * _clutchEngagement;
return I;
}
public override float ForwardStep( float torque, float inertia )
public override float ForwardStep( float torque, float inertiaSum, float dt )
{
if ( !HasOutput )
InputTorque = torque;
InputInertia = inertiaSum;
if ( OutputNameHash == 0 )
return torque;
Torque = Math.Clamp( torque, -SlipTorque, SlipTorque );
Torque = torque * (1 - (1 - MathF.Pow( Pressing, 0.3f )));
// Get the clutch engagement point from the input value
// Do not use the clutchEnagement directly for any calculations!
_clutchEngagement = EngagementCurve.Evaluate( ClutchInput );
_clutchEngagement = Math.Clamp( _clutchEngagement, 0, 1 );
// Calculate output inertia and torque based on the clutch engagement
float returnTorque = Output.ForwardStep( Torque, inertia * Pressing + Inertia ) * Pressing;
// Assume half of the inertia is on the input plate and the other half is on the output clutch plate.
float halfClutchInertia = Inertia * 0.5f;
OutputInertia = (inertiaSum + halfClutchInertia) * _clutchEngagement + halfClutchInertia;
return Math.Clamp( returnTorque, -SlipTorque, SlipTorque );
// Allow the torque output to be only up to the slip torque valu
float outputTorqueClamp = SlipTorque * _clutchEngagement;
OutputTorque = InputTorque;
OutputTorque = Math.Clamp( OutputTorque, 0, outputTorqueClamp );
float slipOverflowTorque = -Math.Min( outputTorqueClamp - OutputTorque, 0 );
// Apply the creep torque commonly caused by torque converter drag in automatic transmissions
ApplyCreepTorque( ref OutputTorque, CreepTorque );
// Send the torque downstream
float returnTorque = _output.ForwardStep( OutputTorque, OutputInertia, dt ) * _clutchEngagement;
// Clamp the return torque to the slip torque of the clutch once again
returnTorque = Math.Clamp( returnTorque, -SlipTorque, SlipTorque );
// Torque returned to the input is a combination of torque returned by the powertrain and the torque that
// was possibly never sent downstream
return returnTorque + slipOverflowTorque;
}
private void ApplyCreepTorque( ref float torque, float creepTorque )
{
// Apply creep torque to forward torque
if ( creepTorque != 0 && Controller.Engine.IsRunning && Controller.TotalSpeed < CreepSpeedLimit )
{
bool torqueWithinCreepRange = torque < creepTorque && torque > -creepTorque;
if ( torqueWithinCreepRange )
{
torque = creepTorque;
}
}
}
}

View File

@@ -0,0 +1,234 @@
using Sandbox;
using System;
namespace VeloX;
public partial class Differential : PowertrainComponent
{
/// <param name="T">Input torque</param>
/// <param name="Wa">Angular velocity of the outputA</param>
/// <param name="Wb">Angular velocity of the outputB</param>
/// <param name="Ia">Inertia of the outputA</param>
/// <param name="Ib">Inertia of the outputB</param>
/// <param name="dt">Time step</param>
/// <param name="biasAB">Torque bias between outputA and outputB. 0 = all torque goes to A, 1 = all torque goes to B</param>
/// <param name="stiffness">Stiffness of the limited slip or locked differential 0-1</param>
/// <param name="powerRamp">Stiffness under power</param>
/// <param name="coastRamp">Stiffness under braking</param>
/// <param name="slipTorque">Slip torque of the limited slip differential</param>
/// <param name="Ta">Torque output towards outputA</param>
/// <param name="Tb">Torque output towards outputB</param>
public delegate void SplitTorque( float T, float Wa, float Wb, float Ia, float Ib, float dt, float biasAB,
float stiffness, float powerRamp, float coastRamp, float slipTorque, out float Ta, out float Tb );
protected override void OnAwake()
{
base.OnAwake();
Name ??= "Differential";
AssignDifferentialDelegate();
}
public enum DifferentialType
{
Open,
Locked,
LimitedSlip,
}
/// <summary>
/// Differential type.
/// </summary>
[Property]
public DifferentialType Type
{
get => _differentialType;
set
{
_differentialType = value;
AssignDifferentialDelegate();
}
}
private DifferentialType _differentialType;
/// <summary>
/// Torque bias between left (A) and right (B) output in [0,1] range.
/// </summary>
[Property, Range( 0, 1 )] public float BiasAB { get; set; } = 0.5f;
/// <summary>
/// Stiffness of locking differential [0,1]. Higher value
/// will result in lower difference in rotational velocity between left and right wheel.
/// Too high value might introduce slight oscillation due to drivetrain windup and a vehicle that is hard to steer.
/// </summary>
[Property, Range( 0, 1 ), HideIf( nameof( _differentialType ), DifferentialType.Open )] public float Stiffness { get; set; } = 0.5f;
/// <summary>
/// Stiffness of the LSD differential under acceleration.
/// </summary>
[Property, Range( 0, 1 ), ShowIf( nameof( _differentialType ), DifferentialType.LimitedSlip )] public float PowerRamp { get; set; } = 1f;
/// <summary>
/// Stiffness of the LSD differential under braking.
/// </summary>
[Property, Range( 0, 1 ), ShowIf( nameof( _differentialType ), DifferentialType.LimitedSlip )] public float CoastRamp { get; set; } = 0.5f;
/// <summary>
/// Second output of differential.
/// </summary>
[Property]
public PowertrainComponent OutputB
{
get { return _outputB; }
set
{
if ( value == this )
{
Log.Warning( $"{Name}: PowertrainComponent Output can not be self." );
OutputBNameHash = 0;
_output = null;
return;
}
if ( _outputB != null )
{
_outputB.InputNameHash = 0;
_outputB.Input = null;
}
_outputB = value;
if ( _outputB != null )
{
_outputB.Input = this;
OutputBNameHash = _outputB.ToString().GetHashCode();
}
else
{
OutputBNameHash = 0;
}
}
}
protected PowertrainComponent _outputB;
public int OutputBNameHash;
/// <summary>
/// Slip torque of limited slip differentials.
/// </summary>
[Property, ShowIf( nameof( _differentialType ), DifferentialType.LimitedSlip )] public float SlipTorque { get; set; } = 400f;
/// <summary>
/// Function delegate that will be used to split the torque between output(A) and outputB.
/// </summary>
public SplitTorque SplitTorqueDelegate;
private void AssignDifferentialDelegate()
{
SplitTorqueDelegate = _differentialType switch
{
DifferentialType.Open => OpenDiffTorqueSplit,
DifferentialType.Locked => LockingDiffTorqueSplit,
DifferentialType.LimitedSlip => LimitedDiffTorqueSplit,
_ => OpenDiffTorqueSplit,
};
}
public static void OpenDiffTorqueSplit( float T, float Wa, float Wb, float Ia, float Ib, float dt, float biasAB,
float stiffness, float powerRamp, float coastRamp, float slipTorque, out float Ta, out float Tb )
{
Ta = T * (1f - biasAB);
Tb = T * biasAB;
}
public static void LockingDiffTorqueSplit( float T, float Wa, float Wb, float Ia, float Ib, float dt, float biasAB,
float stiffness, float powerRamp, float coastRamp, float slipTorque, out float Ta, out float Tb )
{
Ta = T * (1f - biasAB);
Tb = T * biasAB;
float syncTorque = (Wa - Wb) * stiffness * (Ia + Ib) * 0.5f / dt;
Ta -= syncTorque;
Tb += syncTorque;
}
public static void LimitedDiffTorqueSplit( float T, float Wa, float Wb, float Ia, float Ib, float dt, float biasAB,
float stiffness, float powerRamp, float coastRamp, float slipTorque, out float Ta, out float Tb )
{
if ( Wa < 0 || Wb < 0 )
{
Ta = T * (1f - biasAB);
Tb = T * biasAB;
return;
}
// Минимальный момент трения, даже если разницы скоростей нет
float preloadTorque = MathF.Abs( T ) * 0.5f;
float speedDiff = Wa - Wb;
float ramp = T > 0 ? powerRamp : coastRamp;
// Основной момент трения LSD (зависит от разницы скоростей и preload)
float frictionTorque = ramp * (slipTorque * MathF.Abs( speedDiff ) + preloadTorque);
frictionTorque = MathF.Min( frictionTorque, MathF.Abs( T ) * 0.5f );
Ta = T * (1f - biasAB) - MathF.Sign( speedDiff ) * frictionTorque;
Tb = T * biasAB + MathF.Sign( speedDiff ) * frictionTorque;
}
public override float QueryAngularVelocity( float angularVelocity, float dt )
{
InputAngularVelocity = angularVelocity;
if ( OutputNameHash == 0 || OutputBNameHash == 0 )
return angularVelocity;
OutputAngularVelocity = InputAngularVelocity;
float Wa = _output.QueryAngularVelocity( OutputAngularVelocity, dt );
float Wb = _outputB.QueryAngularVelocity( OutputAngularVelocity, dt );
return (Wa + Wb) * 0.5f;
}
public override float QueryInertia()
{
if ( OutputNameHash == 0 || OutputBNameHash == 0 )
return Inertia;
float Ia = _output.QueryInertia();
float Ib = _outputB.QueryInertia();
float I = Inertia + (Ia + Ib);
return I;
}
public override float ForwardStep( float torque, float inertiaSum, float dt )
{
InputTorque = torque;
InputInertia = inertiaSum;
if ( OutputNameHash == 0 || OutputBNameHash == 0 )
return torque;
float Wa = _output.QueryAngularVelocity( OutputAngularVelocity, dt );
float Wb = _outputB.QueryAngularVelocity( OutputAngularVelocity, dt );
float Ia = _output.QueryInertia();
float Ib = _outputB.QueryInertia();
SplitTorqueDelegate.Invoke( torque, Wa, Wb, Ia, Ib, dt, BiasAB, Stiffness, PowerRamp,
CoastRamp, SlipTorque, out float Ta, out float Tb );
float outAInertia = inertiaSum * 0.5f + Ia;
float outBInertia = inertiaSum * 0.5f + Ib;
OutputTorque = Ta + Tb;
OutputInertia = outAInertia + outBInertia;
return _output.ForwardStep( Ta, outAInertia, dt ) + _outputB.ForwardStep( Tb, outBInertia, dt );
}
}

View File

@@ -1,79 +0,0 @@
using Sandbox;
using System;
namespace VeloX.Powertrain;
[Category( "VeloX/Powertrain/Differential" )]
public abstract class BaseDifferential : PowertrainComponent
{
[Property] public float FinalDrive { get; set; } = 3.392f;
[Property] public override float Inertia { get; set; } = 0.01f;
//[Property] public float CoastRamp { get; set; } = 1f;
//[Property] public float PowerRamp { get; set; } = 1f;
//[Property] public float Stiffness { get; set; } = 0.1f;
//[Property] public float SlipTorque { get; set; } = 0f;
//[Property] public float SteerLock { get; set; } = 45f;
/// <summary>
/// The PowertrainComponent this component will output to.
/// </summary>
[Property]
public PowertrainComponent OutputB
{
get => _outputb;
set
{
if ( value == this )
{
_outputb = null;
return;
}
_outputb = value;
if ( _outputb != null )
_outputb.Input = this;
}
}
private PowertrainComponent _outputb;
public override bool HasOutput => Output.IsValid() && OutputB.IsValid();
public override float QueryAngularVelocity( float angularVelocity )
{
this.angularVelocity = angularVelocity;
if ( !HasOutput )
return angularVelocity;
float aW = Output.QueryAngularVelocity( angularVelocity );
float bW = OutputB.QueryAngularVelocity( angularVelocity );
return (aW + bW) * FinalDrive * 0.5f;
}
public abstract void SplitTorque( float aW, float bW, float aI, float bI, out float tqA, out float tqB );
public override float ForwardStep( float torque, float inertia )
{
if ( !HasOutput )
return torque;
float aW = Output.QueryAngularVelocity( angularVelocity );
float bW = OutputB.QueryAngularVelocity( angularVelocity );
float aI = Output.QueryInertia();
float bI = OutputB.QueryInertia();
Torque = torque * FinalDrive;
SplitTorque( aW, bW, aI, bI, out float tqA, out float tqB );
tqA = Output.ForwardStep( tqA, inertia * 0.5f * MathF.Pow( FinalDrive, 2 ) + aI );
tqB = OutputB.ForwardStep( tqB, inertia * 0.5f * MathF.Pow( FinalDrive, 2 ) + bI );
return tqA + tqB;
}
}

View File

@@ -1,13 +0,0 @@
using Sandbox;
namespace VeloX.Powertrain;
public class OpenDifferential : BaseDifferential
{
[Property] public float BiasAB { get; set; } = 0.5f;
public override void SplitTorque( float aW, float bW, float aI, float bI, out float tqA, out float tqB )
{
tqA = Torque * (1 - BiasAB);
tqB = Torque * BiasAB;
}
}

View File

@@ -1,109 +1,663 @@
using Sandbox;
using Sandbox.Audio;
using System;
using System.Collections.Generic;
using System.Linq;
using VeloX.Utils;
using static Sandbox.VertexLayout;
using static VeloX.EngineStream;
namespace VeloX.Powertrain;
namespace VeloX;
public class Engine : PowertrainComponent
public class Engine : PowertrainComponent, IScenePhysicsEvents
{
[Property, Group( "Settings" )] public float IdleRPM { get; set; } = 900f;
[Property, Group( "Settings" )] public float MaxRPM { get; set; } = 7000f;
[Property, Group( "Settings" )] public override float Inertia { get; set; } = 0.151f;
[Property, Group( "Settings" )] public float LimiterDuration { get; set; } = 0.05f;
[Property, Group( "Settings" )] public Curve TorqueMap { get; set; }
[Property, Group( "Settings" )] public EngineStream Stream { get; set; }
protected override void OnAwake()
{
base.OnAwake();
Name ??= "Engine";
UpdatePeakPowerAndTorque();
[Sync] public float Throttle { get; internal set; }
}
[Property] public bool IsRedlining => !limiterTimer;
[Property] public float RPMPercent => Math.Clamp( (RPM - IdleRPM) / (MaxRPM - IdleRPM), 0, 1 );
[Hide] public new bool Input { get; set; }
private float masterThrottle;
private TimeUntil limiterTimer;
private float finalTorque;
public delegate float CalculateTorque( float angularVelocity, float dt );
/// <summary>
/// Delegate for a function that modifies engine power.
/// </summary>
public delegate float PowerModifier();
public enum EngineType
{
ICE,
Electric,
}
/// <summary>
/// If true starter will be ran for [starterRunTime] seconds if engine receives any throttle input.
/// </summary>
[Property] public bool AutoStartOnThrottle { get; set; } = true;
/// <summary>
/// Assign your own delegate to use different type of torque calculation.
/// </summary>
public CalculateTorque CalculateTorqueDelegate;
/// <summary>
/// Engine type. ICE (Internal Combustion Engine) supports features such as starter, stalling, etc.
/// Electric engine (motor) can run in reverse, can not be stalled and does not use starter.
/// </summary>
[Property] public EngineType Type { get; set; } = EngineType.ICE;
/// <summary>
/// Power generated by the engine in kW
/// </summary>
public float generatedPower;
/// <summary>
/// RPM at which idler circuit will try to keep RPMs when there is no input.
/// </summary>
[Property] public float IdleRPM { get; set; } = 900;
/// <summary>
/// Maximum engine power in [kW].
/// </summary>
[Property, Group( "Power" )] public float MaxPower { get; set; } = 120;
/// <summary>
/// Loss power (pumping, friction losses) is calculated as the percentage of maxPower.
/// Should be between 0 and 1 (100%).
/// </summary>
[Range( 0, 1 ), Property] public float EngineLossPercent { get; set; } = 0.8f;
/// <summary>
/// If true the engine will be started immediately, without running the starter, when the vehicle is enabled.
/// Sets engine angular velocity to idle angular velocity.
/// </summary>
[Property] public bool FlyingStartEnabled { get; set; }
[Property] public bool Ignition { get; set; }
/// <summary>
/// Power curve with RPM range [0,1] on the X axis and power coefficient [0,1] on Y axis.
/// Both values are represented as percentages and should be in 0 to 1 range.
/// Power coefficient is multiplied by maxPower to get the final power at given RPM.
/// </summary>
[Property, Group( "Power" )] public Curve PowerCurve { get; set; } = new( new List<Curve.Frame>() { new( 0, 0.4f ), new( 0.16f, 0.65f ), new( 0.32f, 0.85f ), new( 0.64f, 1f ), new( 0.8f, 0.9f ), new( 0.9f, 0.6f ), new( 1f, 0.2f ) } );
/// <summary>
/// Is the engine currently hitting the rev limiter?
/// </summary>
public bool RevLimiterActive;
/// <summary>
/// If engine RPM rises above revLimiterRPM, how long should fuel cutoff last?
/// Higher values make hitting rev limiter more rough and choppy.
/// </summary>
[Property] public float RevLimiterCutoffDuration { get; set; } = 0.12f;
/// <summary>
/// Engine RPM at which rev limiter activates.
/// </summary>
[Property] public float RevLimiterRPM { get; set; } = 6700;
/// <summary>
/// Is the starter currently active?
/// </summary>
[Property, ReadOnly, Group( "Info" )] public bool StarterActive = false;
/// <summary>
/// Torque starter motor can put out. Make sure that this torque is more than loss torque
/// at the starter RPM limit. If too low the engine will fail to start.
/// </summary>
[Property] public float StartDuration = 0.5f;
/// <summary>
/// Peak power as calculated from the power curve. If the power curve peaks at Y=1 peak power will equal max power field value.
/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
/// </summary>
[Property, ReadOnly, Group( "Info" )]
public float EstimatedPeakPower => _peakPower;
private float _peakPower;
/// <summary>
/// RPM at which the peak power is achieved.
/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
/// </summary>
[Property, ReadOnly, Group( "Info" )]
public float EstimatedPeakPowerRPM => _peakPowerRpm;
private float _peakPowerRpm;
/// <summary>
/// Peak torque value as calculated from the power curve.
/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
/// </summary>
[Property, ReadOnly, Group( "Info" )]
public float EstimatedPeakTorque => _peakTorque;
private float _peakTorque;
/// <summary>
/// RPM at which the engine achieves the peak torque, calculated from the power curve.
/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
/// </summary>
[Property, ReadOnly, Group( "Info" )]
public float EstimatedPeakTorqueRPM => _peakTorqueRpm;
private float _peakTorqueRpm;
/// <summary>
/// RPM as a percentage of maximum RPM.
/// </summary>
[Property, ReadOnly, Group( "Info" )]
public float RPMPercent => _rpmPercent;
[Sync] private float _rpmPercent { get; set; }
/// <summary>
/// Engine throttle position. 0 for no throttle and 1 for full throttle.
/// </summary>
[Property, ReadOnly, Group( "Info" )]
public float ThrottlePosition { get; private set; }
/// <summary>
/// Is the engine currently running?
/// Requires ignition to be enabled.
/// </summary>
[Property, ReadOnly, Group( "Info" )]
public bool IsRunning { get; private set; }
/// <summary>
/// Is the engine currently running?
/// Requires ignition to be enabled.
/// </summary>
[Property, ReadOnly, Group( "Info" )]
public bool IsActive { get; private set; }
private float _idleAngularVelocity;
/// <summary>
/// Current load of the engine, based on the power produced.
/// </summary>
public float Load => _load;
public event Action OnEngineStart;
public event Action OnEngineStop;
public event Action OnRevLimiter;
private float _load;
private EngineStreamPlayer StreamPlayer;
public float[] friction = [15.438f, 2.387f, 0.7958f];
protected override void OnStart()
{
base.OnStart();
StreamPlayer = new( Stream );
}
public float GetFrictionTorque( float throttle, float rpm )
{
float s = rpm < 0 ? -1f : 1f;
float r = s * rpm * 0.001f;
float f = friction[0] + friction[1] * r + friction[2] * r * r;
return -s * f * (1 - throttle);
}
private float GenerateTorque()
{
float throttle = Throttle;
float rpm = RPM;
float friction = GetFrictionTorque( throttle, rpm );
float maxInitialTorque = TorqueMap.Evaluate( RPMPercent ) - friction;
float idleFadeStart = Math.Clamp( MathX.Remap( rpm, IdleRPM - 300, IdleRPM, 1, 0 ), 0, 1 );
float idleFadeEnd = Math.Clamp( MathX.Remap( rpm, IdleRPM, IdleRPM + 600, 1, 0 ), 0, 1 );
float additionalEnergySupply = idleFadeEnd * (-friction / maxInitialTorque) + idleFadeStart;
if ( rpm > MaxRPM )
if ( Type == EngineType.ICE )
{
throttle = 0;
limiterTimer = LimiterDuration;
CalculateTorqueDelegate = CalculateTorqueICE;
}
else if ( !limiterTimer )
throttle = 0;
masterThrottle = Math.Clamp( additionalEnergySupply + throttle, 0, 1 );
float realInitialTorque = maxInitialTorque * masterThrottle;
Torque = realInitialTorque + friction;
return Torque;
}
public override float ForwardStep( float _, float __ )
{
if ( !HasOutput )
else if ( Type == EngineType.Electric )
{
angularVelocity += GenerateTorque() / Inertia * Time.Delta;
angularVelocity = Math.Max( angularVelocity, 0 );
return 0;
IdleRPM = 0f;
FlyingStartEnabled = true;
CalculateTorqueDelegate = CalculateTorqueElectric;
StarterActive = false;
StartDuration = 0.001f;
RevLimiterCutoffDuration = 0f;
}
float outputInertia = Output.QueryInertia();
float inertiaSum = Inertia + outputInertia;
float outputW = Output.QueryAngularVelocity( angularVelocity );
float targetW = Inertia / inertiaSum * angularVelocity + outputInertia / inertiaSum * outputW;
float generatedTorque = GenerateTorque();
float reactTorque = (targetW - angularVelocity) * Inertia / Time.Delta;
float returnedTorque = Output.ForwardStep( generatedTorque - reactTorque, Inertia );
finalTorque = generatedTorque + reactTorque + returnedTorque;
angularVelocity += finalTorque / inertiaSum * Time.Delta;
angularVelocity = Math.Max( angularVelocity, 0 );
UpdateStream();
return finalTorque;
}
private void UpdateStream()
public void StartEngine()
{
if ( StreamPlayer is null )
if ( IsRunning ) return;
Ignition = true;
OnEngineStart?.Invoke();
if ( Type != EngineType.Electric )
{
if ( FlyingStartEnabled )
{
FlyingStart();
}
else if ( !StarterActive && Controller != null )
{
StarterCoroutine();
}
}
}
private async void StarterCoroutine()
{
if ( Type == EngineType.Electric || StarterActive )
return;
StreamPlayer.Throttle = Throttle;
StreamPlayer.RPMPercent = RPMPercent;
StreamPlayer.EngineState = EngineState.Running;
StreamPlayer.IsRedlining = IsRedlining;
try
{
float startTimer = 0f;
StarterActive = true;
// Ensure safe start duration
StartDuration = Math.Max( 0.1f, StartDuration );
_starterTorque = ((_idleAngularVelocity - OutputAngularVelocity) * Inertia) / StartDuration;
while ( startTimer <= StartDuration && StarterActive )
{
startTimer += 0.1f;
await GameTask.DelaySeconds( 0.1f );
}
}
finally
{
_starterTorque = 0;
StarterActive = false;
IsActive = true;
}
}
private void FlyingStart()
{
Ignition = true;
StarterActive = false;
OutputAngularVelocity = IdleRPM.RPMToAngularVelocity();
}
public void StopEngine()
{
Ignition = false;
IsActive = true;
OnEngineStop?.Invoke();
}
/// <summary>
/// Toggles engine state.
/// </summary>
public void StartStopEngine()
{
if ( IsRunning )
StopEngine();
else
StartEngine();
}
public void UpdatePeakPowerAndTorque()
{
GetPeakPower( out _peakPower, out _peakPowerRpm );
GetPeakTorque( out _peakTorque, out _peakTorqueRpm );
}
public void UpdateEngine( in float dt )
{
StreamEngineUpdate( dt, WorldPosition );
if ( IsProxy )
return;
// Cache values
_userThrottleInput = _userThrottleInput.LerpTo( Controller.SwappedThrottle, 0.05f );
ThrottlePosition = _userThrottleInput;
_idleAngularVelocity = IdleRPM.RPMToAngularVelocity();
_revLimiterAngularVelocity = RevLimiterRPM.RPMToAngularVelocity();
if ( _revLimiterAngularVelocity == 0f )
return;
// Check for start on throttle
if ( !IsRunning && !StarterActive && AutoStartOnThrottle && ThrottlePosition > 0.2f )
StartEngine();
bool wasRunning = IsRunning;
IsRunning = Ignition;
if ( wasRunning && !IsRunning )
StopEngine();
// Physics update
if ( OutputNameHash == 0 )
return;
float drivetrainInertia = _output.QueryInertia();
float inertiaSum = Inertia + drivetrainInertia;
if ( inertiaSum == 0 )
return;
float drivetrainAngularVelocity = QueryAngularVelocity( OutputAngularVelocity, dt );
float targetAngularVelocity = Inertia / inertiaSum * OutputAngularVelocity + drivetrainInertia / inertiaSum * drivetrainAngularVelocity;
// Calculate generated torque and power
float generatedTorque = CalculateTorqueICE( OutputAngularVelocity, dt );
generatedPower = TorqueToPowerInKW( in OutputAngularVelocity, in generatedTorque );
// Calculate reaction torque
float reactionTorque = (targetAngularVelocity - OutputAngularVelocity) * Inertia / dt;
// Calculate/get torque returned from wheels
OutputTorque = generatedTorque - reactionTorque;
float returnTorque = -ForwardStep( OutputTorque, 0, dt );
float totalTorque = generatedTorque + returnTorque + reactionTorque;
OutputAngularVelocity += totalTorque / inertiaSum * dt;
// Clamp the angular velocity to prevent any powertrain instabilities over the limits
OutputAngularVelocity = Math.Clamp( OutputAngularVelocity, 0, _revLimiterAngularVelocity * 1.05f );
// Calculate cached values
_rpmPercent = Math.Clamp( OutputAngularVelocity / _revLimiterAngularVelocity, 0, 1 );
_load = Math.Clamp( generatedPower / MaxPower, 0, 1 );
StreamPlayer.Update( Time.Delta, WorldPosition );
}
private float _starterTorque;
private float _revLimiterAngularVelocity;
private float _userThrottleInput;
private async void RevLimiter()
{
if ( RevLimiterActive || Type == EngineType.Electric || RevLimiterCutoffDuration == 0 )
return;
RevLimiterActive = true;
OnRevLimiter?.Invoke();
await GameTask.DelayRealtimeSeconds( RevLimiterCutoffDuration );
RevLimiterActive = false;
}
/// <summary>
/// Calculates torque for electric engine type.
/// </summary>
public float CalculateTorqueElectric( float angularVelocity, float dt )
{
float absAngVel = Math.Abs( angularVelocity );
// Avoid angular velocity spikes while shifting
if ( Controller.Transmission.IsShifting )
ThrottlePosition = 0;
float maxLossPower = MaxPower * 0.3f;
float lossPower = maxLossPower * (1f - ThrottlePosition) * RPMPercent;
float genPower = MaxPower * ThrottlePosition;
float totalPower = genPower - lossPower;
totalPower = MathX.Lerp( totalPower * 0.1f, totalPower, RPMPercent * 10f );
float clampedAngVel = absAngVel < 10f ? 10f : absAngVel;
return PowerInKWToTorque( clampedAngVel, totalPower );
}
/// <summary>
/// Calculates torque for ICE (Internal Combustion Engine).
/// </summary>
public float CalculateTorqueICE( float angularVelocity, float dt )
{
// Set the throttle to 0 when shifting, but avoid doing so around idle RPM to prevent stalls.
if ( Controller.Transmission.IsShifting && angularVelocity > _idleAngularVelocity )
ThrottlePosition = 0f;
// Set throttle to 0 when starter active.
if ( StarterActive )
ThrottlePosition = 0f;
// Apply idle throttle correction to keep the engine running
else
ApplyICEIdleCorrection();
// Trigger rev limiter if needed
if ( angularVelocity >= _revLimiterAngularVelocity && !RevLimiterActive )
RevLimiter();
// Calculate torque
float generatedTorque;
// Do not generate any torque while starter is active to prevent RPM spike during startup
// or while stalled to prevent accidental starts.
if ( StarterActive )
generatedTorque = 0f;
else
generatedTorque = CalculateICEGeneratedTorqueFromPowerCurve();
float lossTorque = (StarterActive || ThrottlePosition > 0.2f) ? 0f : CalculateICELossTorqueFromPowerCurve();
// Reduce the loss torque at rev limiter, but allow it to be >0 to prevent vehicle getting
// stuck at rev limiter.
if ( RevLimiterActive )
lossTorque *= 0.25f;
generatedTorque += _starterTorque + lossTorque;
return generatedTorque;
}
private float CalculateICELossTorqueFromPowerCurve()
{
// Avoid issues with large torque spike around 0 angular velocity.
if ( OutputAngularVelocity < 10f )
return -OutputAngularVelocity * MaxPower * 0.03f;
float angVelPercent = OutputAngularVelocity < 10f ? 0.1f : Math.Clamp( OutputAngularVelocity, 0, _revLimiterAngularVelocity ) / _revLimiterAngularVelocity;
float lossPower = angVelPercent * 3f * -MaxPower * Math.Clamp( _userThrottleInput + 0.5f, 0, 1 ) * EngineLossPercent;
return PowerInKWToTorque( OutputAngularVelocity, lossPower );
}
private void ApplyICEIdleCorrection()
{
if ( Ignition && OutputAngularVelocity < _idleAngularVelocity * 1.1f )
{
// Apply a small correction to account for the error since the throttle is applied only
// if the idle RPM is below the target RPM.
float idleCorrection = _idleAngularVelocity * 1.08f - OutputAngularVelocity;
idleCorrection = idleCorrection < 0f ? 0f : idleCorrection;
float idleThrottlePosition = Math.Clamp( idleCorrection * 0.01f, 0, 1 );
ThrottlePosition = Math.Max( _userThrottleInput, idleThrottlePosition );
}
}
private float CalculateICEGeneratedTorqueFromPowerCurve()
{
generatedPower = 0;
float torque = 0;
if ( !Ignition && !StarterActive )
return 0;
if ( RevLimiterActive )
ThrottlePosition = 0.2f;
else
{
// Add maximum losses to the maximum power when calculating the generated power since the maxPower is net value (after losses).
generatedPower = PowerCurve.Evaluate( _rpmPercent ) * (MaxPower * (1f + EngineLossPercent)) * ThrottlePosition;
torque = PowerInKWToTorque( OutputAngularVelocity, generatedPower );
}
return torque;
}
public void GetPeakTorque( out float peakTorque, out float peakTorqueRpm )
{
peakTorque = 0;
peakTorqueRpm = 0;
for ( float i = 0.05f; i < 1f; i += 0.05f )
{
float rpm = i * RevLimiterRPM;
float P = PowerCurve.Evaluate( i ) * MaxPower;
if ( rpm < IdleRPM )
{
continue;
}
float W = rpm.RPMToAngularVelocity();
float T = (P * 1000f) / W;
if ( T > peakTorque )
{
peakTorque = T;
peakTorqueRpm = rpm;
}
}
}
public void GetPeakPower( out float peakPower, out float peakPowerRpm )
{
float maxY = 0f;
float maxX = 1f;
for ( float i = 0f; i < 1f; i += 0.05f )
{
float y = PowerCurve.Evaluate( i );
if ( y > maxY )
{
maxY = y;
maxX = i;
}
}
peakPower = maxY * MaxPower;
peakPowerRpm = maxX * RevLimiterRPM;
}
[Property, Group( "Parsing" )] string Clipboard { get; set; }
[Button, Group( "Parsing" )]
public void FromLUT()
{
if ( Clipboard == null )
return;
using var undo = Scene.Editor?.UndoScope( "From LUT" ).WithComponentChanges( this ).Push();
var data = new List<(float RPM, float PowerHP)>();
var lines = Clipboard.Split( ['\n', '\r'], StringSplitOptions.RemoveEmptyEntries );
foreach ( var line in lines )
{
var parts = line.Split( '|' );
if ( parts.Length == 2 &&
float.TryParse( parts[0], out float rpm ) &&
float.TryParse( parts[1], out float powerHP ) )
{
data.Add( (rpm, powerHP) );
}
}
RevLimiterRPM = data.Max( x => x.RPM );
MaxPower = data.Max( x => x.PowerHP ) * 0.746f;
var frames = data.Select( d =>
new Curve.Frame(
d.RPM / RevLimiterRPM,
(d.PowerHP * 0.746f) / MaxPower
)
).ToList();
PowerCurve = new( frames );
Clipboard = null;
}
[Property] public EngineStream Stream { get; set; }
public bool EngineSoundPaused => !IsRunning;
private float _wobbleTime;
private static readonly Mixer EngineMixer = Mixer.FindMixerByName( "Car Engine" );
private readonly Dictionary<Layer, SoundHandle> EngineSounds = [];
protected override void OnDestroy()
{
base.OnDestroy();
foreach ( var item in EngineSounds.Values )
item.Dispose();
}
public void StreamEngineUpdate( float deltaTime, Vector3 position, bool isLocal = false )
{
var globalPitch = 1.0f;
// Gear wobble effect
if ( _wobbleTime > 0 )
{
_wobbleTime -= deltaTime * (0.1f + ThrottlePosition);
globalPitch += MathF.Cos( _wobbleTime * Stream.Parameters.WobbleFrequency ) * _wobbleTime * (1 - _wobbleTime) * Stream.Parameters.WobbleStrength;
}
globalPitch *= Stream.Parameters.Pitch;
// Redline effect
var redlineVolume = 1.0f;
if ( RPMPercent >= 0.95f )
{
redlineVolume = 1 - Stream.Parameters.RedlineStrength +
MathF.Cos( Time.Now * Stream.Parameters.RedlineFrequency ) *
Stream.Parameters.RedlineStrength;
}
// Process layers
foreach ( var (id, layer) in Stream.Layers )
{
EngineSounds.TryGetValue( layer, out var channel );
if ( !channel.IsValid() )
{
channel = Sound.PlayFile( layer.AudioPath );
EngineSounds[layer] = channel;
}
if ( channel.Paused && EngineSoundPaused )
continue;
// Reset controller outputs
float layerVolume = 1.0f;
float layerPitch = 1.0f;
// Apply all controllers
foreach ( var controller in layer.Controllers )
{
var inputValue = controller.InputParameter switch
{
Audio.Controller.InputTypes.Throttle => _userThrottleInput,
Audio.Controller.InputTypes.RpmFraction => RPMPercent,
_ => 0.0f
};
var normalized = Math.Clamp( inputValue, controller.InputRange.Min, controller.InputRange.Max );
var outputValue = controller.InputRange.Remap(
normalized,
controller.OutputRange.Min,
controller.OutputRange.Max
);
// Apply to correct parameter
switch ( controller.OutputParameter )
{
case Audio.Controller.OutputTypes.Volume:
layerVolume *= outputValue;
break;
case Audio.Controller.OutputTypes.Pitch:
layerPitch *= outputValue;
break;
}
}
// Apply redline effect if needed
layerVolume *= layer.UseRedline ? redlineVolume : 1.0f;
layerPitch *= globalPitch;
// Update audio channel
channel.Pitch = layerPitch;
channel.Volume = layerVolume * Stream.Parameters.Volume;
channel.Position = position;
channel.ListenLocal = isLocal;
channel.Paused = EngineSoundPaused;
channel.TargetMixer = EngineMixer;
}
}
}

View File

@@ -1,45 +0,0 @@
using Sandbox;
using System;
namespace VeloX.Powertrain;
[Category( "VeloX/Powertrain/Gearbox" )]
public abstract class BaseGearbox : PowertrainComponent
{
[Property] public override float Inertia { get; set; } = 0.01f;
protected float ratio;
public override float QueryInertia()
{
if ( !HasOutput || ratio == 0 )
return Inertia;
return Inertia + Output.QueryInertia() / MathF.Pow( ratio, 2 );
}
public override float QueryAngularVelocity( float angularVelocity )
{
this.angularVelocity = angularVelocity;
if ( !HasOutput || ratio == 0 )
return angularVelocity;
return Output.QueryAngularVelocity( angularVelocity ) * ratio;
}
public override float ForwardStep( float torque, float inertia )
{
Torque = torque * ratio;
if ( !HasOutput )
return torque;
if ( ratio == 0 )
{
Output.ForwardStep( 0, Inertia * 0.5f );
return torque;
}
return Output.ForwardStep( Torque, (inertia + Inertia) * MathF.Pow( ratio, 2 ) ) / ratio;
}
}

View File

@@ -1,52 +0,0 @@
using Sandbox;
namespace VeloX.Powertrain;
public class ManualGearbox : BaseGearbox
{
[Property] public float[] Ratios { get; set; } = [3.626f, 2.200f, 1.541f, 1.213f, 1.000f, 0.767f];
[Property] public float Reverse { get; set; } = 3.4f;
[Property, InputAction] public string ForwardAction { get; set; } = "Attack1";
[Property, InputAction] public string BackwardAction { get; set; } = "Attack2";
private int gear;
protected void SetGear( int gear )
{
if ( gear < -1 || gear >= Ratios.Length )
return;
this.gear = gear;
RecalcRatio();
}
private void RecalcRatio()
{
if ( gear == -1 )
ratio = -Reverse;
else if ( gear == 0 )
ratio = 0;
else
ratio = Ratios[gear - 1];
}
public void Shift( int dir )
{
SetGear( gear + dir );
}
private void InputResolve()
{
if ( Sandbox.Input.Pressed( ForwardAction ) )
Shift( 1 );
else if ( Sandbox.Input.Pressed( BackwardAction ) )
Shift( -1 );
}
public override float ForwardStep( float torque, float inertia )
{
InputResolve();
return base.ForwardStep( torque, inertia );
}
}

View File

@@ -1,34 +0,0 @@
using Sandbox;
using System;
namespace VeloX.Powertrain;
public class PowerWheel : PowertrainComponent
{
[Property] public VeloXWheel Wheel { get; set; }
public override float QueryInertia()
{
float dtScale = Math.Clamp( Time.Delta, 0.01f, 0.05f ) / 0.005f;
return Wheel.BaseInertia * dtScale;
}
public override float QueryAngularVelocity( float angularVelocity )
{
return Wheel.AngularVelocity;
}
public override float ForwardStep( float torque, float inertia )
{
Wheel.AutoPhysics = false;
Wheel.Torque = torque;
Wheel.Brake = Vehicle.Brake;
Inertia = Wheel.BaseInertia + inertia;
Wheel.Inertia = inertia;
Wheel.DoPhysics( Vehicle );
angularVelocity = Wheel.AngularVelocity;
return Wheel.CounterTorque;
}
}

View File

@@ -1,26 +1,31 @@
using Sandbox;
using System;
using System;
using Sandbox;
namespace VeloX.Powertrain;
namespace VeloX;
public abstract class PowertrainComponent : Component
{
protected override void OnAwake()
{
Vehicle ??= Components.Get<VeloXBase>( FindMode.EverythingInSelfAndAncestors );
}
[Property] public VeloXBase Controller;
/// <summary>
/// Name of the component. Only unique names should be used on the same vehicle.
/// </summary>
[Property] public string Name { get; set; }
public const float RAD_TO_RPM = 60f / MathF.Tau;
public const float RPM_TO_RAD = 1 / (60 / MathF.Tau);
public const float UNITS_PER_METER = 39.37f;
public const float UNITS_TO_METERS = 0.01905f;
public const float KG_TO_N = 9.80665f;
public const float KG_TO_KN = 0.00980665f;
[Property] public VeloXBase Vehicle { get; set; }
[Property] public virtual float Inertia { get; set; } = 0.02f;
/// <summary>
/// Angular inertia of the component. Higher inertia value will result in a powertrain that is slower to spin up, but
/// also slower to spin down. Too high values will result in (apparent) sluggish response while too low values will
/// result in vehicle being easy to stall and possible powertrain instability / glitches.
/// </summary>
[Property, Range( 0.0002f, 2f )] public float Inertia { get; set; } = 0.05f;
[Property, ReadOnly] public float InputTorque;
[Property, ReadOnly] public float OutputTorque;
public float InputAngularVelocity;
public float OutputAngularVelocity;
public float InputInertia;
public float OutputInertia;
/// <summary>
/// Input component. Set automatically.
@@ -34,67 +39,143 @@ public abstract class PowertrainComponent : Component
if ( value == null || value == this )
{
_input = null;
InputNameHash = 0;
return;
}
_input = value;
InputNameHash = _input.GetHashCode();
}
}
private PowertrainComponent _input;
protected PowertrainComponent _input;
public int InputNameHash;
/// <summary>
/// The PowertrainComponent this component will output to.
/// </summary>
[Property]
public PowertrainComponent Output
{
get => _output;
get { return _output; }
set
{
if ( value == this )
{
Log.Warning( $"{Name}: PowertrainComponent Output can not be self." );
OutputNameHash = 0;
_output = null;
return;
}
_output = value;
if ( _output != null )
{
_output.Input = this;
OutputNameHash = _output.GetHashCode();
}
else
{
OutputNameHash = 0;
}
}
}
private PowertrainComponent _output;
protected PowertrainComponent _output;
public int OutputNameHash { get; private set; }
public float RPM => angularVelocity * RAD_TO_RPM;
protected float angularVelocity;
protected float Torque;
/// <summary>
/// Input shaft RPM of component.
/// </summary>
[Property, ReadOnly]
public float InputRPM => AngularVelocityToRPM( InputAngularVelocity );
public virtual bool HasOutput => Output.IsValid();
/// <summary>
/// Output shaft RPM of component.
/// </summary>
[Property, ReadOnly]
public float OutputRPM => AngularVelocityToRPM( OutputAngularVelocity );
public virtual float QueryAngularVelocity( float angularVelocity, float dt )
{
InputAngularVelocity = angularVelocity;
if ( OutputNameHash == 0 )
{
return angularVelocity;
}
OutputAngularVelocity = angularVelocity;
return _output.QueryAngularVelocity( OutputAngularVelocity, dt );
}
public virtual float QueryInertia()
{
if ( !HasOutput )
if ( OutputNameHash == 0 )
return Inertia;
return Inertia + Output.QueryInertia();
}
float Ii = Inertia;
float Ia = _output.QueryInertia();
public virtual float QueryAngularVelocity( float angularVelocity )
return Ii + Ia;
}
public virtual float ForwardStep( float torque, float inertiaSum, float dt )
{
if ( !HasOutput )
return angularVelocity;
InputTorque = torque;
InputInertia = inertiaSum;
return Output.QueryAngularVelocity( angularVelocity );
if ( OutputNameHash == 0 )
return torque;
OutputTorque = InputTorque;
OutputInertia = inertiaSum + Inertia;
return _output.ForwardStep( OutputTorque, OutputInertia, dt );
}
public virtual float ForwardStep( float torque, float inertia )
public static float TorqueToPowerInKW( in float angularVelocity, in float torque )
{
if ( !HasOutput )
return Torque;
// Power (W) = Torque (Nm) * Angular Velocity (rad/s)
float powerInWatts = torque * angularVelocity;
return Output.ForwardStep( Torque, inertia + Inertia );
// Convert power from watts to kilowatts
float powerInKW = powerInWatts / 1000f;
return powerInKW;
}
public static float PowerInKWToTorque( in float angularVelocity, in float powerInKW )
{
// Convert power from kilowatts to watts
float powerInWatts = powerInKW * 1000f;
// Torque (Nm) = Power (W) / Angular Velocity (rad/s)
float absAngVel = Math.Abs( angularVelocity );
float clampedAngularVelocity = (absAngVel > -1f && absAngVel < 1f) ? 1f : angularVelocity;
float torque = powerInWatts / clampedAngularVelocity;
return torque;
}
public float CalculateOutputPowerInKW()
{
return GetPowerInKW( OutputTorque, OutputAngularVelocity );
}
public static float GetPowerInKW( in float torque, in float angularVelocity )
{
// Power (W) = Torque (Nm) * Angular Velocity (rad/s)
float powerInWatts = torque * angularVelocity;
// Convert power from watts to kilowatts
float powerInKW = powerInWatts / 1000f;
return powerInKW;
}
public static float AngularVelocityToRPM( float angularVelocity ) => angularVelocity * 9.5492965855137f;
public static float RPMToAngularVelocity( float RPM ) => RPM * 0.10471975511966f;
}

View File

@@ -0,0 +1,800 @@
using Sandbox;
using System.Collections.Generic;
using System;
namespace VeloX;
public class Transmission : PowertrainComponent
{
protected override void OnAwake()
{
base.OnAwake();
Name ??= "Transmission";
LoadGearsFromGearingProfile();
}
/// <summary>
/// A class representing a single ground surface type.
/// </summary>
public partial class TransmissionGearingProfile
{
/// <summary>
/// List of forward gear ratios starting from 1st forward gear.
/// </summary>
public List<float> ForwardGears { get; set; } = [3.59f, 2.02f, 1.38f, 1f, 0.87f];
/// <summary>
/// List of reverse gear ratios starting from 1st reverse gear.
/// </summary>
public List<float> ReverseGears { get; set; } = [-4f,];
}
public const float INPUT_DEADZONE = 0.05f;
public float ReferenceShiftRPM => _referenceShiftRPM;
private float _referenceShiftRPM;
/// <summary>
/// If true the gear input has to be held for the transmission to stay in gear, otherwise it goes to neutral.
/// Used for hardware H-shifters.
/// </summary>
[Property] public bool HoldToKeepInGear { get; set; }
/// <summary>
/// Final gear multiplier. Each gear gets multiplied by this value.
/// Equivalent to axle/differential ratio in real life.
/// </summary>
[Property] public float FinalGearRatio { get; set; } = 4.3f;
/// <summary>
/// [Obsolete, will be removed]
/// Currently active gearing profile.
/// Final gear ratio will be determined from this and final gear ratio.
/// </summary>
[Property] public TransmissionGearingProfile GearingProfile { get; set; } = new();
/// <summary>
/// A list of gears ratios in order of negative, neutral and then positive.
/// E.g. -4, 0, 6, 4, 3, 2 => one reverse, 4 forward gears.
/// </summary>
[Property, ReadOnly, Group( "Info" )] public List<float> Gears = new();
/// <summary>
/// Number of forward gears.
/// </summary>
public int ForwardGearCount;
/// <summary>
/// Number of reverse gears.
/// </summary>
public int ReverseGearCount;
/// <summary>
/// How much inclines affect shift point position. Higher value will push the shift up and shift down RPM up depending
/// on the current incline to prevent vehicle from upshifting at the wrong time.
/// </summary>
[Property, Range( 0, 4 )] public float InclineEffectCoeff { get; set; }
/// <summary>
/// Function that handles gear shifts.
/// Use External transmission type and assign this delegate manually to use a custom
/// gear shift function.
/// </summary>
public delegate void Shift( VeloXBase vc );
/// <summary>
/// Function that changes the gears as required.
/// Use transmissionType External and assign this delegate to use your own gear shift code.
/// </summary>
public Shift ShiftDelegate;
/// <summary>
/// Event that gets triggered when transmission shifts down.
/// </summary>
public event Action OnGearDownShift;
/// <summary>
/// Event that gets triggered when transmission shifts (up or down).
/// </summary>
public event Action OnGearShift;
/// <summary>
/// Event that gets triggered when transmission shifts up.
/// </summary>
public event Action OnGearUpShift;
/// <summary>
/// Time after shifting in which shifting can not be done again.
/// </summary>
[Property] public float PostShiftBan { get; set; } = 0.5f;
public enum AutomaticTransmissionDNRShiftType
{
Auto,
RequireShiftInput,
RepeatInput,
}
/// <summary>
/// Behavior when switching from neutral to forward or reverse gear.
/// </summary>
[Property] public AutomaticTransmissionDNRShiftType DNRShiftType { get; set; } = AutomaticTransmissionDNRShiftType.Auto;
/// <summary>
/// Speed at which the vehicle can switch between D/N/R gears.
/// </summary>
[Property] public float DnrSpeedThreshold { get; set; } = 0.4f;
/// <summary>
/// If set to >0, the clutch will need to be released to the value below the set number
/// for gear shifts to occur.
/// </summary>
[Property, Range( 0, 1 )] public float ClutchInputShiftThreshold { get; set; } = 1f;
/// <summary>
/// Time it takes transmission to shift between gears.
/// </summary>
[Property] public float ShiftDuration { get; set; } = 0.2f;
/// <summary>
/// Intensity of variable shift point. Higher value will result in shift point moving higher up with higher engine
/// load.
/// </summary>
[Property, Range( 0, 1 )] public float VariableShiftIntensity { get; set; } = 0.3f;
/// <summary>
/// If enabled transmission will adjust both shift up and down points to match current load.
/// </summary>
[Property] public bool VariableShiftPoint { get; set; } = true;
/// <summary>
/// Current gear ratio.
/// </summary>
[Property, ReadOnly] public float CurrentGearRatio { get; set; }
/// <summary>
/// Is the transmission currently in the post-shift phase in which the shifting is disabled/banned to prevent gear hunting?
/// </summary>
[Property, ReadOnly] public bool IsPostShiftBanActive { get; set; }
/// <summary>
/// Is a gear shift currently in progress.
/// </summary>
[Property, ReadOnly] public bool IsShifting { get; set; }
/// <summary>
/// Progress of the current gear shift in range of 0 to 1.
/// </summary>
[Property, ReadOnly] public float ShiftProgress { get; set; }
/// <summary>
/// Current RPM at which transmission will aim to downshift. All the modifiers are taken into account.
/// This value changes with driving conditions.
/// </summary>
[Property]
public float DownshiftRPM
{
get => _downshiftRPM;
set { _downshiftRPM = Math.Clamp( value, 0, float.MaxValue ); }
}
private float _downshiftRPM = 1400;
/// <summary>
/// RPM at which the transmission will try to downshift, but the value might get changed by shift modifier such
/// as incline modifier.
/// To get actual downshift RPM use DownshiftRPM.
/// </summary>
[Property]
public float TargetDownshiftRPM => _targetDownshiftRPM;
private float _targetDownshiftRPM;
/// <summary>
/// RPM at which automatic transmission will shift up. If dynamic shift point is enabled this value will change
/// depending on load.
/// </summary>
[Property]
public float UpshiftRPM
{
get => _upshiftRPM;
set { _upshiftRPM = Math.Clamp( value, 0, float.MaxValue ); }
}
private float _upshiftRPM = 2800;
/// <summary>
/// RPM at which the transmission will try to upshift, but the value might get changed by shift modifier such
/// as incline modifier.
/// To get actual upshift RPM use UpshiftRPM.
/// </summary>
[Property]
public float TargetUpshiftRPM => _targetUpshiftRPM;
private float _targetUpshiftRPM;
public enum TransmissionShiftType
{
Manual,
Automatic,
//CVT,
}
/// <summary>
/// Determines in which way gears can be changed.
/// Manual - gears can only be shifted by manual user input.
/// Automatic - automatic gear changing. Allows for gear skipping (e.g. 3rd->5th) which can be useful in trucks and
/// other high gear count vehicles.
/// AutomaticSequential - automatic gear changing but only one gear at the time can be shifted (e.g. 3rd->4th)
/// </summary>
[Property]
public TransmissionShiftType TransmissionType
{
get => transmissionType; set
{
if ( value == transmissionType )
return;
transmissionType = value;
AssignShiftDelegate();
}
}
/// <summary>
/// Is the automatic gearbox sequential?
/// Has no effect on manual transmission.
/// </summary>
[Property] public bool IsSequential { get; set; } = false;
[Property] public bool AllowUpshiftGearSkipping { get; set; }
[Property] public bool AllowDownshiftGearSkipping { get; set; } = true;
private bool _repeatInputFlag;
private float _smoothedThrottleInput;
/// <summary>
/// Timer needed to prevent manual transmission from slipping out of gear too soon when hold in gear is enabled,
/// which could happen in FixedUpdate() runs twice for one Update() and the shift flag is reset
/// resulting in gearbox thinking it has no shift input.
/// </summary>
private float _slipOutOfGearTimer = -999f;
/// <summary>
/// 0 for neutral, less than 0 for reverse gears and lager than 0 for forward gears.
/// Use 'ShiftInto' to set gear.
/// </summary>
[Property, Sync]
public int Gear
{
get => IndexToGear( gearIndex );
set => gearIndex = GearToIndex( value );
}
/// <summary>
/// Current gear index in the gears list.
/// Different from gear because gear uses -1 = R, 0 = N and D = 1, while this is the apsolute index
/// in the range of 0 to gear list size minus one.
/// Use Gear to get the actual gear.
/// </summary>
public int gearIndex;
private TransmissionShiftType transmissionType = TransmissionShiftType.Automatic;
private int GearToIndex( int g )
{
return g + ReverseGearCount;
}
private int IndexToGear( int g )
{
return g - ReverseGearCount;
}
/// <summary>
/// Returns current gear name as a string, e.g. "R", "R2", "N" or "1"
/// </summary>
public string GearName
{
get
{
int gear = Gear;
if ( _gearNameCache.TryGetValue( gear, out string gearName ) )
return gearName;
if ( gear == 0 )
gearName = "N";
else if ( gear > 0 )
gearName = Gear.ToString();
else
gearName = "R" + (ReverseGearCount > 1 ? -gear : "");
_gearNameCache[gear] = gearName;
return gearName;
}
}
private readonly Dictionary<int, string> _gearNameCache = new();
public void LoadGearsFromGearingProfile()
{
if ( GearingProfile == null )
return;
int totalGears = GearingProfile.ReverseGears.Count + 1 + GearingProfile.ForwardGears.Count;
if ( Gears == null )
Gears = new( totalGears );
else
{
Gears.Clear();
Gears.Capacity = totalGears;
}
Gears.AddRange( GearingProfile.ReverseGears );
Gears.Add( 0 );
Gears.AddRange( GearingProfile.ForwardGears );
}
protected override void OnStart()
{
base.OnStart();
LoadGearsFromGearingProfile();
UpdateGearCounts();
Gear = 0;
AssignShiftDelegate();
}
/// <summary>
/// Total gear ratio of the transmission for current gear.
/// </summary>
private float CalculateTotalGearRatio()
{
//if ( TransmissionType == TransmissionShiftType.CVT )
//{
// float minRatio = Gears[gearIndex];
// float maxRatio = minRatio * 40f;
// float t = Math.Clamp( Controller.Engine.RPMPercent + (1f - Controller.Engine.ThrottlePosition), 0, 1 );
// float ratio = MathX.Lerp( maxRatio, minRatio, t ) * FinalGearRatio;
// return MathX.Lerp( CurrentGearRatio, ratio, Time.Delta * 5f );
//}
//else
return Gears[gearIndex] * FinalGearRatio;
}
public override float QueryAngularVelocity( float angularVelocity, float dt )
{
InputAngularVelocity = angularVelocity;
if ( CurrentGearRatio == 0 || OutputNameHash == 0 )
{
OutputAngularVelocity = 0f;
return angularVelocity;
}
OutputAngularVelocity = InputAngularVelocity / CurrentGearRatio;
return _output.QueryAngularVelocity( OutputAngularVelocity, dt ) * CurrentGearRatio;
}
public override float QueryInertia()
{
if ( OutputNameHash == 0 || CurrentGearRatio == 0 )
return Inertia;
return Inertia + _output.QueryInertia() / (CurrentGearRatio * CurrentGearRatio);
}
/// <summary>
/// Calculates the would-be RPM if none of the wheels was slipping.
/// </summary>
/// <returns>RPM as it would be if the wheels are not slipping or in the air.</returns>
private float CalculateNoSlipRPM()
{
float vehicleLocalVelocity = Controller.LocalVelocity.x.InchToMeter();
// Get the average no-slip wheel RPM
// Use the vehicle velocity as the friction velocities for the wheel are 0 when in air and
// because the shift RPM is not really required to be extremely precise, so slight offset
// between the vehicle position and velocity and the wheel ones is not important.
// Still, calculate for each wheel since radius might be different.
float angVelSum = 0f;
foreach ( VeloXWheel wheelComponent in Controller.MotorWheels )
{
angVelSum += vehicleLocalVelocity / wheelComponent.Radius;
}
// Apply total gear ratio to get the no-slip condition RPM
return AngularVelocityToRPM( angVelSum / Controller.MotorWheels.Count ) * CurrentGearRatio;
}
public override float ForwardStep( float torque, float inertiaSum, float dt )
{
InputTorque = torque;
InputInertia = inertiaSum;
UpdateGearCounts();
if ( _output == null )
return InputTorque;
// Update current gear ratio
CurrentGearRatio = CalculateTotalGearRatio();
// Run the shift function
_referenceShiftRPM = CalculateNoSlipRPM();
ShiftDelegate?.Invoke( Controller );
// Reset any input related to shifting, now that the shifting has been processed
//Controller.Input.ResetShiftFlags();
// Run the physics step
// No output, simply return the torque to the sender
if ( OutputNameHash == 0 )
return torque;
// In neutral, do not send any torque but update components downstram
if ( CurrentGearRatio < 1e-5f && CurrentGearRatio > -1e-5f )
{
OutputTorque = 0;
OutputInertia = InputInertia;
_output.ForwardStep( OutputTorque, OutputInertia, dt );
return torque;
}
// Always send torque to keep wheels updated
OutputTorque = torque * CurrentGearRatio;
OutputInertia = (inertiaSum + Inertia) * (CurrentGearRatio * CurrentGearRatio);
return _output.ForwardStep( torque * CurrentGearRatio, OutputInertia, dt ) / CurrentGearRatio;
}
private void UpdateGearCounts()
{
ForwardGearCount = 0;
ReverseGearCount = 0;
int gearCount = Gears.Count;
for ( int i = 0; i < gearCount; i++ )
{
float gear = Gears[i];
if ( gear > 0 )
ForwardGearCount++;
else if ( gear < 0 )
ReverseGearCount++;
}
}
private void AssignShiftDelegate()
{
if ( TransmissionType == TransmissionShiftType.Manual )
ShiftDelegate = ManualShift;
else if ( TransmissionType == TransmissionShiftType.Automatic )
ShiftDelegate = AutomaticShift;
//else if ( TransmissionType == TransmissionShiftType.CVT )
// ShiftDelegate = CVTShift;
}
private void ManualShift( VeloXBase car )
{
if ( car.IsShiftingUp )
{
ShiftInto( Gear + 1 );
return;
}
if ( car.IsShiftingDown )
{
ShiftInto( Gear - 1 );
return;
}
if ( HoldToKeepInGear )
{
_slipOutOfGearTimer += Time.Delta;
if ( Gear != 0 && _slipOutOfGearTimer > 0.1f )
ShiftInto( 0 );
}
}
/// <summary>
/// Shifts into given gear. 0 for neutral, less than 0 for reverse and above 0 for forward gears.
/// Does nothing if the target gear is equal to current gear.
/// </summary>
public void ShiftInto( int targetGear, bool instant = false )
{
// Clutch is not pressed above the set threshold, exit and do not shift.
if ( Controller.IsClutching > ClutchInputShiftThreshold )
return;
int currentGear = Gear;
bool isShiftFromOrToNeutral = targetGear == 0 || currentGear == 0;
//Debug.Log($"Shift from {currentGear} into {targetGear}");
// Check if shift can happen at all
if ( targetGear == currentGear || targetGear < -100 )
return;
// Convert gear to gear list index
int targetIndex = GearToIndex( targetGear );
// Check for gear list bounds
if ( targetIndex < 0 || targetIndex >= Gears.Count )
return;
if ( !IsShifting && (isShiftFromOrToNeutral || !IsPostShiftBanActive) )
{
ShiftCoroutine( currentGear, targetGear, isShiftFromOrToNeutral || instant );
// If in neutral reset the repeated input flat required for repeat input reverse
if ( targetGear == 0 )
_repeatInputFlag = false;
}
}
private async void ShiftCoroutine( int currentGear, int targetGear, bool instant )
{
if ( IsShifting )
return;
float dt = Time.Delta;
bool isManual = TransmissionType == TransmissionShiftType.Manual;
//Debug.Log($"Shift from {currentGear} to {targetGear}, instant: {instant}");
// Immediately start shift ban to prevent repeated shifts while this one has not finished
if ( !isManual )
IsPostShiftBanActive = true;
IsShifting = true;
ShiftProgress = 0f;
// Run the first half of shift timer
float shiftTimer = 0;
float halfDuration = ShiftDuration * 0.5f;
if ( !instant )
while ( shiftTimer < halfDuration )
{
ShiftProgress = shiftTimer / ShiftDuration;
shiftTimer += dt;
await GameTask.DelayRealtimeSeconds( dt );
}
// Do the shift at the half point of shift duration
Gear = targetGear;
if ( currentGear < targetGear )
OnGearUpShift?.Invoke();
else
OnGearDownShift?.Invoke();
OnGearShift?.Invoke();
// Run the second half of the shift timer
if ( !instant )
while ( shiftTimer < ShiftDuration )
{
ShiftProgress = shiftTimer / ShiftDuration;
shiftTimer += dt;
await GameTask.DelayRealtimeSeconds( dt );
}
// Shift has finished
ShiftProgress = 1f;
IsShifting = false;
// Run post shift ban only if not manual as blocking user input feels unresponsive and post shift ban
// exists to prevent auto transmission from hunting.
if ( !isManual )
{
// Post shift ban timer
float postShiftBanTimer = 0;
while ( postShiftBanTimer < PostShiftBan )
{
postShiftBanTimer += dt;
await GameTask.DelayRealtimeSeconds( dt );
}
// Post shift ban has finished
IsPostShiftBanActive = false;
}
}
private void CVTShift( VeloXBase car ) => AutomaticShift( car );
/// <summary>
/// Handles automatic and automatic sequential shifting.
/// </summary>
private void AutomaticShift( VeloXBase car )
{
float vehicleSpeed = car.ForwardSpeed;
float throttleInput = car.SwappedThrottle;
float brakeInput = car.SwappedBrakes;
int currentGear = Gear;
// Assign base shift points
_targetDownshiftRPM = _downshiftRPM;
_targetUpshiftRPM = _upshiftRPM;
// Calculate shift points for variable shift RPM
if ( VariableShiftPoint )
{
// Smooth throttle input so that the variable shift point does not shift suddenly and cause gear hunting
_smoothedThrottleInput = MathX.Lerp( _smoothedThrottleInput, throttleInput, Time.Delta * 2f );
float revLimiterRPM = car.Engine.RevLimiterRPM;
_targetUpshiftRPM = _upshiftRPM + Math.Clamp( _smoothedThrottleInput * VariableShiftIntensity, 0f, 1f ) * _upshiftRPM;
_targetUpshiftRPM = Math.Clamp( _targetUpshiftRPM, _upshiftRPM, revLimiterRPM * 0.97f );
_targetDownshiftRPM = _downshiftRPM + Math.Clamp( _smoothedThrottleInput * VariableShiftIntensity, 0f, 1f ) * _downshiftRPM;
_targetDownshiftRPM = Math.Clamp( _targetDownshiftRPM, car.Engine.IdleRPM * 1.1f, _targetUpshiftRPM * 0.7f );
// Add incline modifier
float inclineModifier = Math.Clamp( car.WorldRotation.Forward.Dot( Vector3.Up ) * InclineEffectCoeff, 0f, 1f );
_targetUpshiftRPM += revLimiterRPM * inclineModifier;
_targetDownshiftRPM += revLimiterRPM * inclineModifier;
}
// In neutral
if ( currentGear == 0 )
{
if ( DNRShiftType == AutomaticTransmissionDNRShiftType.Auto )
{
if ( throttleInput > INPUT_DEADZONE )
ShiftInto( 1 );
else if ( brakeInput > INPUT_DEADZONE )
ShiftInto( -1 );
}
else if ( DNRShiftType == AutomaticTransmissionDNRShiftType.RequireShiftInput )
{
if ( car.IsShiftingUp )
ShiftInto( 1 );
else if ( car.IsShiftingDown )
ShiftInto( -1 );
}
else if ( DNRShiftType == AutomaticTransmissionDNRShiftType.RepeatInput )
{
if ( _repeatInputFlag == false && throttleInput < INPUT_DEADZONE && brakeInput < INPUT_DEADZONE )
_repeatInputFlag = true;
if ( _repeatInputFlag )
{
if ( throttleInput > INPUT_DEADZONE )
ShiftInto( 1 );
else if ( brakeInput > INPUT_DEADZONE )
ShiftInto( -1 );
}
}
}
// In reverse
else if ( currentGear < 0 )
{
// Shift into neutral
if ( DNRShiftType == AutomaticTransmissionDNRShiftType.RequireShiftInput )
{
if ( car.IsShiftingUp )
ShiftInto( 0 );
}
else
{
if ( vehicleSpeed < DnrSpeedThreshold && (brakeInput > INPUT_DEADZONE || throttleInput < INPUT_DEADZONE) )
ShiftInto( 0 );
}
// Reverse upshift
float absGearMinusOne = currentGear - 1;
absGearMinusOne = absGearMinusOne < 0 ? -absGearMinusOne : absGearMinusOne;
if ( _referenceShiftRPM > TargetUpshiftRPM && absGearMinusOne < ReverseGearCount )
ShiftInto( currentGear - 1 );
// Reverse downshift
else if ( _referenceShiftRPM < TargetDownshiftRPM && currentGear < -1 )
ShiftInto( currentGear + 1 );
}
// In forward
else
{
if ( vehicleSpeed > 0.4f )
{
// Upshift
if ( currentGear < ForwardGearCount && _referenceShiftRPM > TargetUpshiftRPM )
{
if ( !IsSequential && AllowUpshiftGearSkipping )
{
int g = currentGear;
while ( g < ForwardGearCount )
{
g++;
float wouldBeEngineRPM = ReverseTransmitRPM( _referenceShiftRPM / CurrentGearRatio, g );
float shiftDurationPadding = Math.Clamp( ShiftDuration, 0, 1 ) * (_targetUpshiftRPM - _targetDownshiftRPM) * 0.25f;
if ( wouldBeEngineRPM < _targetDownshiftRPM + shiftDurationPadding )
{
g--;
break;
}
}
if ( g != currentGear )
{
ShiftInto( g );
}
}
else
{
ShiftInto( currentGear + 1 );
}
}
// Downshift
else if ( _referenceShiftRPM < TargetDownshiftRPM )
{
// Non-sequential
if ( !IsSequential && AllowDownshiftGearSkipping )
{
if ( currentGear != 1 )
{
int g = currentGear;
while ( g > 1 )
{
g--;
float wouldBeEngineRPM = ReverseTransmitRPM( _referenceShiftRPM / CurrentGearRatio, g );
if ( wouldBeEngineRPM > _targetUpshiftRPM )
{
g++;
break;
}
}
if ( g != currentGear )
{
ShiftInto( g );
}
}
else if ( vehicleSpeed < DnrSpeedThreshold && throttleInput < INPUT_DEADZONE
&& DNRShiftType !=
AutomaticTransmissionDNRShiftType
.RequireShiftInput )
{
ShiftInto( 0 );
}
}
// Sequential
else
{
if ( currentGear != 1 )
{
ShiftInto( currentGear - 1 );
}
else if ( vehicleSpeed < DnrSpeedThreshold && throttleInput < INPUT_DEADZONE &&
brakeInput < INPUT_DEADZONE
&& DNRShiftType !=
AutomaticTransmissionDNRShiftType.RequireShiftInput )
{
ShiftInto( 0 );
}
}
}
}
// Shift into neutral
else
{
if ( DNRShiftType != AutomaticTransmissionDNRShiftType.RequireShiftInput )
{
if ( throttleInput < INPUT_DEADZONE )
{
ShiftInto( 0 );
}
}
else
{
if ( car.IsShiftingDown )
{
ShiftInto( 0 );
}
}
}
}
}
/// <summary>
/// Converts axle RPM to engine RPM for given gear in Gears list.
/// </summary>
public float ReverseTransmitRPM( float inputRPM, int g )
{
float outRpm = inputRPM * Gears[GearToIndex( g )] * FinalGearRatio;
return Math.Abs( outRpm );
}
}

View File

@@ -0,0 +1,59 @@
using Sandbox;
using System;
namespace VeloX;
public partial class WheelPowertrain : PowertrainComponent
{
protected override void OnAwake()
{
base.OnAwake();
Name ??= Wheel.ToString();
}
[Property] public VeloXWheel Wheel { get; set; }
protected override void OnStart()
{
_initialRollingResistance = Wheel.RollingResistanceTorque;
_initialWheelInertia = Wheel.BaseInertia;
}
private float _initialRollingResistance;
private float _initialWheelInertia;
public override float QueryAngularVelocity( float angularVelocity, float dt )
{
InputAngularVelocity = OutputAngularVelocity = Wheel.AngularVelocity;
return OutputAngularVelocity;
}
public override float QueryInertia()
{
// Calculate the base inertia of the wheel and scale it by the inverse of the dt.
float dtScale = Math.Clamp( Time.Delta, 0.01f, 0.05f ) / 0.005f;
float radius = Wheel.Radius;
return 0.5f * Wheel.Mass * radius * radius * dtScale;
}
public void ApplyRollingResistanceMultiplier( float multiplier )
{
Wheel.RollingResistanceTorque = _initialRollingResistance * multiplier;
}
public override float ForwardStep( float torque, float inertiaSum, float dt )
{
InputTorque = torque;
InputInertia = inertiaSum;
OutputTorque = InputTorque;
OutputInertia = Wheel.BaseInertia + inertiaSum;
Wheel.Torque = OutputTorque;
Wheel.Inertia = OutputInertia;
Wheel.AutoSimulate = false;
Wheel.StepPhys( Controller, dt );
return Math.Abs( Wheel.CounterTorque );
}
}