using Sandbox; using System.Collections.Generic; using System; namespace VeloX; public class Transmission : PowertrainComponent { protected override void OnAwake() { base.OnAwake(); Name ??= "Transmission"; LoadGearsFromGearingProfile(); } /// /// A class representing a single ground surface type. /// public partial class TransmissionGearingProfile { /// /// List of forward gear ratios starting from 1st forward gear. /// public List ForwardGears { get; set; } = [3.59f, 2.02f, 1.38f, 1f, 0.87f]; /// /// List of reverse gear ratios starting from 1st reverse gear. /// public List ReverseGears { get; set; } = [-4f,]; } public const float INPUT_DEADZONE = 0.05f; public float ReferenceShiftRPM => _referenceShiftRPM; private float _referenceShiftRPM; /// /// 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. /// [Property] public bool HoldToKeepInGear { get; set; } /// /// Final gear multiplier. Each gear gets multiplied by this value. /// Equivalent to axle/differential ratio in real life. /// [Property] public float FinalGearRatio { get; set; } = 4.3f; /// /// [Obsolete, will be removed] /// Currently active gearing profile. /// Final gear ratio will be determined from this and final gear ratio. /// [Property] public TransmissionGearingProfile GearingProfile { get; set; } = new(); /// /// 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. /// [Property, ReadOnly, Group( "Info" )] public List Gears = new(); /// /// Number of forward gears. /// public int ForwardGearCount; /// /// Number of reverse gears. /// public int ReverseGearCount; /// /// 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. /// [Property, Range( 0, 4 )] public float InclineEffectCoeff { get; set; } /// /// Function that handles gear shifts. /// Use External transmission type and assign this delegate manually to use a custom /// gear shift function. /// public delegate void Shift( VeloXBase vc ); /// /// Function that changes the gears as required. /// Use transmissionType External and assign this delegate to use your own gear shift code. /// public Shift ShiftDelegate; /// /// Event that gets triggered when transmission shifts down. /// public event Action OnGearDownShift; /// /// Event that gets triggered when transmission shifts (up or down). /// public event Action OnGearShift; /// /// Event that gets triggered when transmission shifts up. /// public event Action OnGearUpShift; /// /// Time after shifting in which shifting can not be done again. /// [Property] public float PostShiftBan { get; set; } = 0.5f; public enum AutomaticTransmissionDNRShiftType { Auto, RequireShiftInput, RepeatInput, } /// /// Behavior when switching from neutral to forward or reverse gear. /// [Property] public AutomaticTransmissionDNRShiftType DNRShiftType { get; set; } = AutomaticTransmissionDNRShiftType.Auto; /// /// Speed at which the vehicle can switch between D/N/R gears. /// [Property] public float DnrSpeedThreshold { get; set; } = 0.4f; /// /// If set to >0, the clutch will need to be released to the value below the set number /// for gear shifts to occur. /// [Property, Range( 0, 1 )] public float ClutchInputShiftThreshold { get; set; } = 1f; /// /// Time it takes transmission to shift between gears. /// [Property] public float ShiftDuration { get; set; } = 0.2f; /// /// Intensity of variable shift point. Higher value will result in shift point moving higher up with higher engine /// load. /// [Property, Range( 0, 1 )] public float VariableShiftIntensity { get; set; } = 0.3f; /// /// If enabled transmission will adjust both shift up and down points to match current load. /// [Property] public bool VariableShiftPoint { get; set; } = true; /// /// Current gear ratio. /// [Property, ReadOnly] public float CurrentGearRatio { get; set; } /// /// Is the transmission currently in the post-shift phase in which the shifting is disabled/banned to prevent gear hunting? /// [Property, ReadOnly] public bool IsPostShiftBanActive { get; set; } /// /// Is a gear shift currently in progress. /// [Property, ReadOnly] public bool IsShifting { get; set; } /// /// Progress of the current gear shift in range of 0 to 1. /// [Property, ReadOnly] public float ShiftProgress { get; set; } /// /// Current RPM at which transmission will aim to downshift. All the modifiers are taken into account. /// This value changes with driving conditions. /// [Property] public float DownshiftRPM { get => _downshiftRPM; set { _downshiftRPM = Math.Clamp( value, 0, float.MaxValue ); } } private float _downshiftRPM = 1400; /// /// 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. /// [Property] public float TargetDownshiftRPM => _targetDownshiftRPM; private float _targetDownshiftRPM; /// /// RPM at which automatic transmission will shift up. If dynamic shift point is enabled this value will change /// depending on load. /// [Property] public float UpshiftRPM { get => _upshiftRPM; set { _upshiftRPM = Math.Clamp( value, 0, float.MaxValue ); } } private float _upshiftRPM = 2800; /// /// 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. /// [Property] public float TargetUpshiftRPM => _targetUpshiftRPM; private float _targetUpshiftRPM; public enum TransmissionShiftType { Manual, Automatic, //CVT, } /// /// 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) /// [Property] public TransmissionShiftType TransmissionType { get => transmissionType; set { if ( value == transmissionType ) return; transmissionType = value; AssignShiftDelegate(); } } /// /// Is the automatic gearbox sequential? /// Has no effect on manual transmission. /// [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; /// /// 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. /// private float _slipOutOfGearTimer = -999f; /// /// 0 for neutral, less than 0 for reverse gears and lager than 0 for forward gears. /// Use 'ShiftInto' to set gear. /// [Property, Sync] public int Gear { get => IndexToGear( gearIndex ); set => gearIndex = GearToIndex( value ); } /// /// 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. /// public int gearIndex; private TransmissionShiftType transmissionType = TransmissionShiftType.Automatic; private int GearToIndex( int g ) { return g + ReverseGearCount; } private int IndexToGear( int g ) { return g - ReverseGearCount; } /// /// Returns current gear name as a string, e.g. "R", "R2", "N" or "1" /// 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 _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(); } /// /// Total gear ratio of the transmission for current gear. /// 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); } /// /// Calculates the would-be RPM if none of the wheels was slipping. /// /// RPM as it would be if the wheels are not slipping or in the air. 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 ); } } /// /// 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. /// 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 ); /// /// Handles automatic and automatic sequential shifting. /// 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 ); } } } } } /// /// Converts axle RPM to engine RPM for given gear in Gears list. /// public float ReverseTransmitRPM( float inputRPM, int g ) { float outRpm = inputRPM * Gears[GearToIndex( g )] * FinalGearRatio; return Math.Abs( outRpm ); } }