677 lines
18 KiB
C#
677 lines
18 KiB
C#
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;
|
|
|
|
public class Engine : PowertrainComponent, IScenePhysicsEvents
|
|
{
|
|
protected override void OnAwake()
|
|
{
|
|
base.OnAwake();
|
|
Name ??= "Engine";
|
|
UpdatePeakPowerAndTorque();
|
|
|
|
}
|
|
|
|
[Hide] public new bool Input { get; set; }
|
|
|
|
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; private 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;
|
|
|
|
|
|
protected override void OnStart()
|
|
{
|
|
base.OnStart();
|
|
|
|
if ( Type == EngineType.ICE )
|
|
{
|
|
CalculateTorqueDelegate = CalculateTorqueICE;
|
|
}
|
|
else if ( Type == EngineType.Electric )
|
|
{
|
|
IdleRPM = 0f;
|
|
FlyingStartEnabled = true;
|
|
CalculateTorqueDelegate = CalculateTorqueElectric;
|
|
StarterActive = false;
|
|
StartDuration = 0.001f;
|
|
RevLimiterCutoffDuration = 0f;
|
|
}
|
|
}
|
|
|
|
public void StartEngine()
|
|
{
|
|
if ( IsRunning ) return;
|
|
|
|
Ignition = true;
|
|
OnEngineStart?.Invoke();
|
|
|
|
if ( Type != EngineType.Electric )
|
|
{
|
|
if ( FlyingStartEnabled )
|
|
{
|
|
FlyingStart();
|
|
IsRunning = true;
|
|
IsActive = true;
|
|
}
|
|
else if ( !StarterActive && Controller != null )
|
|
{
|
|
StarterCoroutine();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
IsRunning = true;
|
|
IsActive = true;
|
|
}
|
|
}
|
|
|
|
private async void StarterCoroutine()
|
|
{
|
|
if ( Type == EngineType.Electric || StarterActive )
|
|
return;
|
|
|
|
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 Task.DelaySeconds( 0.1f );
|
|
|
|
if ( OutputAngularVelocity >= _idleAngularVelocity * 0.8f )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_starterTorque = 0;
|
|
StarterActive = false;
|
|
IsActive = true;
|
|
IsRunning = true;
|
|
}
|
|
}
|
|
|
|
|
|
private void FlyingStart()
|
|
{
|
|
Ignition = true;
|
|
StarterActive = false;
|
|
OutputAngularVelocity = IdleRPM.RPMToAngularVelocity();
|
|
IsRunning = true;
|
|
IsActive = true;
|
|
}
|
|
|
|
public void StopEngine()
|
|
{
|
|
Ignition = false;
|
|
IsRunning = false;
|
|
IsActive = false;
|
|
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;
|
|
|
|
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 );
|
|
|
|
|
|
}
|
|
|
|
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 Task.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, 1f );
|
|
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;
|
|
|
|
}
|
|
}
|
|
}
|