prerelease
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
234
Code/Base/Powertrain/Differential.cs
Normal file
234
Code/Base/Powertrain/Differential.cs
Normal 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 );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
800
Code/Base/Powertrain/Transmission.cs
Normal file
800
Code/Base/Powertrain/Transmission.cs
Normal 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 );
|
||||
}
|
||||
|
||||
}
|
||||
59
Code/Base/Powertrain/WheelPowertrain.cs
Normal file
59
Code/Base/Powertrain/WheelPowertrain.cs
Normal 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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user