Code/Exhaust.cs
using System;
using System.Collections.Generic;
using Meteor.VehicleTool.Vehicle.Powertrain;
using Sandbox;
using Sandbox.Audio;
namespace Meteor.VehicleTool;
public sealed class Exhaust : Component
{
[Property, Group( "Components" )] public Engine Engine { get; set; }
/// <summary>
/// How much soot is emitted when throttle is pressed.
/// </summary>
[Property, Group( "Particle" ), Range( 0, 1 )] public float SootIntensity { get; set; } = 0.4f;
/// <summary>
/// Particle start speed is multiplied by this value based on engine RPM.
/// </summary>
[Property, Group( "Particle" ), Range( 1, 5 )] public float MaxSpeedMultiplier { get; set; } = 1.4f;
/// <summary>
/// Particle start size is multiplied by this value based on engine RPM.
/// </summary>
[Property, Group( "Particle" ), Range( 1, 5 )] public float MaxSizeMultiplier { get; set; } = 1.2f;
/// <summary>
/// Normal particle start color. Used when there is no throttle - engine is under no load.
/// </summary>
[Property, Group( "Particle" )] public Color NormalColor { get; set; } = new( 0.6f, 0.6f, 0.6f, 0.3f );
/// <summary>
/// Soot particle start color. Used under heavy throttle - engine is under load.
/// </summary>
[Property, Group( "Particle" )] public Color SootColor { get; set; } = new( 0.1f, 0.1f, 0.8f );
static Texture SmokeTexture { get; set; } = Texture.Load( "materials/particles/smoke/render/smokeloop_i_0.vtex_c" );
[Property, Group( "Components" ), RequireComponent] public ParticleConeEmitter Emitter { get; set; }
[Property, Group( "Components" ), RequireComponent] public ParticleSpriteRenderer Renderer { get; set; }
[Property, Group( "Components" ), RequireComponent] public ParticleEffect Effect { get; set; }
[Property, Group( "Sound" ), Range( 0, 1 )] public MixerHandle SoundMixer { get; set; } = Mixer.Master;
[Property, Group( "Sound" )] public SoundEvent PopSounds { get; set; }
[Property, Group( "Sound" ), Range( 0f, 1f )] public float PopChance { get; set; } = 0.1f;
[Property, Group( "Sound" )] public Dictionary<int, SoundFile> ExhaustSounds { get; set; }
[Property, Group( "Sound" ), Range( 0f, 2f )] public float ExhaustVolume { get; set; } = 1;
private Dictionary<int, SoundHandle> SoundHandles { get; set; }
private List<int> SoundTimes { get; set; }
private float SmoothValue { get; set; }
private float SmoothVolume { get; set; }
protected override void OnDestroy()
{
RemoveSounds();
}
private void RemoveSounds()
{
if ( SoundHandles is not null )
foreach ( var item in SoundHandles.Values )
item.Stop();
}
private void UpdateSound()
{
SmoothValue = SmoothValue * (1 - 0.2f) + Engine.OutputRPM * 0.2f;
SmoothVolume = SmoothVolume * (1 - 0.1f) + Engine.ThrottlePosition * 0.1f;
var isRunning = Engine.IsRunning;
for ( int n = 0; n < SoundTimes.Count; n++ )
{
var time = SoundTimes[n]; // this
float min = (n == 0) ? -100000f : SoundTimes[n - 1]; // prev
float max = n == (SoundTimes.Count - 1) ? 100000f : SoundTimes[n + 1]; // next
float c = MathM.Fade( SmoothValue, min - 10f, time, max + 10 );
float vol = c * MathM.Map( SmoothVolume, 0f, 1f, 0.5f, 1f ) * ExhaustVolume;
SoundHandle soundObject = SoundHandles[time];
soundObject.Volume = isRunning ? vol : 0f;
soundObject.Pitch = SmoothValue / time;
soundObject.Position = WorldPosition;
}
}
private async void LoadSoundsAsync()
{
SoundTimes = [];
SoundHandles = [];
foreach ( KeyValuePair<int, SoundFile> item in ExhaustSounds )
{
await item.Value.LoadAsync();
SoundHandle snd = Sound.PlayFile( item.Value );
snd.Volume = 0;
snd.TargetMixer = SoundMixer.GetOrDefault();
snd.Occlusion = true;
snd.Distance = 25000;
snd.Falloff = new Curve( new Curve.Frame( 0f, 1f, 0f, -1.8f ), new Curve.Frame( 0.05f, 0.08f, 3.5f, -3.5f ), new Curve.Frame( 0.2f, 0.04f, 0.16f, -0.16f ), new Curve.Frame( 1f, 0f ) );
SoundTimes.Add( item.Key );
SoundHandles.Add( item.Key, snd );
}
}
private void OnRevLimiter()
{
if ( !PopSounds.IsValid() )
return;
if ( Random.Shared.Float( 0, 1 ) < PopChance )
{
var snd = Sound.Play( PopSounds );
snd.Position = WorldPosition;
}
}
[Button]
public void SetupSmoke()
{
Effect ??= Components.GetOrCreate<ParticleEffect>( FindMode.InSelf );
Effect.MaxParticles = 1000;
Effect.Lifetime = new()
{
Type = ParticleFloat.ValueType.Range,
Evaluation = ParticleFloat.EvaluationType.Particle,
ConstantA = 1.2f,
ConstantB = 2f,
};
Effect.StartVelocity = 0;
Effect.Damping = 1f;
Effect.ApplyRotation = true;
Effect.Roll = new()
{
Type = ParticleFloat.ValueType.Range,
Evaluation = ParticleFloat.EvaluationType.Particle,
ConstantA = 0,
ConstantB = 360,
};
Effect.ApplyShape = true;
Effect.Scale = new()
{
Type = ParticleFloat.ValueType.Curve,
Evaluation = ParticleFloat.EvaluationType.Life,
CurveA = new( new List<Curve.Frame>() { new( 0, 0 ), new( 0.05f, 5f ), new( 1f, 20f ) } ),
};
Effect.ApplyColor = true;
Effect.Alpha = new()
{
Type = ParticleFloat.ValueType.Curve,
Evaluation = ParticleFloat.EvaluationType.Particle,
CurveA = new( new List<Curve.Frame>() { new( 0, 0 ), new( 0.2f, 0.5f ), new( 1, 0 ) } ),
};
Effect.ApplyAlpha = true;
Effect.Gradient = new()
{
Type = ParticleGradient.ValueType.Range,
Evaluation = ParticleGradient.EvaluationType.Life,
ConstantA = Color.White,
ConstantB = Color.Transparent,
};
Effect.Space = ParticleEffect.SimulationSpace.Local;
Effect.Force = true;
Effect.ForceDirection = new Vector3( 120, 0, 20 );
Effect.Space = ParticleEffect.SimulationSpace.Local;
Effect.SheetSequence = true;
Effect.SequenceSpeed = 0.5f;
Effect.SequenceTime = 1f;
Renderer ??= Components.GetOrCreate<ParticleSpriteRenderer>( FindMode.InSelf );
Renderer.Texture = SmokeTexture;
Renderer.MotionBlur = true;
Emitter ??= Components.GetOrCreate<ParticleConeEmitter>( FindMode.InSelf );
Emitter.ConeAngle = 22;
Emitter.ConeFar = 15;
Emitter.Burst = 1;
Emitter.Rate = 50;
}
private float _initStartSpeedMin;
private float _initStartSpeedMax;
private float _initStartSizeMin;
private float _initStartSizeMax;
private float _sootAmount;
private ParticleFloat _minMaxCurve;
protected override void OnAwake()
{
if ( ExhaustSounds.Count > 0 )
LoadSoundsAsync();
}
protected override void OnStart()
{
_initStartSpeedMin = Effect.StartVelocity.ConstantA;
_initStartSpeedMax = Effect.StartVelocity.ConstantB;
_initStartSizeMin = Effect.Scale.ConstantA;
_initStartSizeMax = Effect.Scale.ConstantB;
Engine.OnRevLimiter += OnRevLimiter;
}
protected override void OnUpdate()
{
if ( SoundTimes is not null )
UpdateSound();
float engineLoad = Engine.Load;
float rpmPercent = Engine.RPMPercent;
_sootAmount = engineLoad * SootIntensity;
Effect.Enabled = Engine.IsRunning;
// Color
Effect.Tint = Color.Lerp( Effect.Tint, Color.Lerp( NormalColor, SootColor, _sootAmount ), Time.Delta * 7f );
Effect.Tint = Effect.Tint.WithAlphaMultiplied( 10 / (Engine.Controller.CurrentSpeed + 10) );
// Speed
float speedMultiplier = MaxSpeedMultiplier - 1f;
_minMaxCurve = Effect.StartVelocity;
_minMaxCurve.ConstantA = _initStartSpeedMin + rpmPercent * speedMultiplier;
_minMaxCurve.ConstantB = _initStartSpeedMax + rpmPercent * speedMultiplier;
Effect.StartVelocity = _minMaxCurve;
// Size
float sizeMultiplier = MaxSizeMultiplier - 1f;
_minMaxCurve = Effect.Scale;
_minMaxCurve.ConstantA = _initStartSizeMin + rpmPercent * sizeMultiplier;
_minMaxCurve.ConstantB = _initStartSizeMax + rpmPercent * sizeMultiplier;
Effect.Scale = _minMaxCurve;
}
}