velox/Code/Base/Powertrain/Transmission.cs
2025-12-03 15:17:36 +07:00

801 lines
23 KiB
C#

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 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;
/// <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 Task.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 Task.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 Task.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 = car.Engine.EstimatedPeakPowerRPM - 2000;
_targetUpshiftRPM = car.Engine.EstimatedPeakPowerRPM;
// 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 );
}
}