Vehicles/VehicleSkids.cs

Static helper that spawns tire skid mark quads under sliding vehicle wheels. Called per wheel per frame by the vehicle, it lays a flat model segment along each step of the wheel path, fades old segments and caps the total at 700. Runs as local client only effects, nothing networked.

namespace HorizonRP;

/// <summary>
/// Les traces de pneus sur la route : tant qu'une roue glisse, chaque bout de chemin parcouru
/// devient un carré de gomme (matériau translucide trace_pneu.vmat) posé à plat sur le sol,
/// dans l'axe de la roue. Les traces pâlissent puis s'effacent. Chaque machine dessine les
/// siennes d'après ce qu'elle voit (rien ne passe par le réseau) ; le véhicule appelle
/// <see cref="Mark"/> à chaque image pour chaque roue.
///
/// Un LineRenderer « à plat » ne s'affichait pas (tracé par la tranche) : d'où ces carrés,
/// un seul modèle partagé, posé, tourné et étiré par segment.
/// </summary>
public static class VehicleSkids
{
	/// <summary>Durée de vie d'une trace, et nombre de segments gardés en tout.</summary>
	const float Life = 60f;
	const float FadeTime = 10f;
	const int MaxSegments = 700;
	/// <summary>Longueur d'un segment.</summary>
	const float Step = 14f;

	sealed class Segment
	{
		public GameObject Go;
		public ModelRenderer Renderer;
		public RealTimeSince Born;
	}

	static readonly Dictionary<(Guid, int), Vector3> Last = new();
	static readonly Queue<Segment> All = new();

	/// <summary>La roue <paramref name="wheel"/> du véhicule glisse (ou plus) à cet endroit du sol.</summary>
	public static void Mark( Vehicle vehicle, int wheel, bool skidding, Vector3 ground, Vector3 normal, float width )
	{
		var key = (vehicle.GameObject.Id, wheel);
		if ( !skidding )
		{
			Last.Remove( key );
			return;
		}

		var point = ground + normal * 0.4f;
		if ( !Last.TryGetValue( key, out var from ) || from.Distance( point ) > Step * 6 )
		{
			Last[key] = point;
			return;
		}
		if ( from.Distance( point ) < Step )
			return;

		Add( vehicle.Scene, from, point, normal, MathF.Max( 4f, width * 0.8f ) );
		Last[key] = point;
		Cleanup();
	}

	static void Add( Scene scene, Vector3 from, Vector3 to, Vector3 normal, float width )
	{
		var model = Quad();
		if ( model is null )
			return;
		using var _ = scene.Push();
		var go = new GameObject( true, "Trace de pneu" ) { NetworkMode = NetworkMode.Never };
		var dir = (to - from).Normal;
		go.WorldPosition = (from + to) / 2;
		// X le long du chemin, Z selon la normale du sol ; un peu plus long pour que les
		// segments se chevauchent sans trou.
		go.WorldRotation = Rotation.LookAt( dir, normal );
		go.WorldScale = new Vector3( from.Distance( to ) + 2f, width, 1 );
		var r = go.Components.Create<ModelRenderer>();
		r.Model = model;
		r.RenderType = ModelRenderer.ShadowRenderType.Off;
		All.Enqueue( new Segment { Go = go, Renderer = r, Born = 0 } );
		while ( All.Count > MaxSegments )
			Remove( All.Dequeue() );
	}

	/// <summary>Les vieilles traces pâlissent puis disparaissent (dans l'ordre de création).</summary>
	static void Cleanup()
	{
		foreach ( var s in All )
		{
			var left = Life - s.Born;
			if ( left < FadeTime && s.Renderer.IsValid() )
				s.Renderer.Tint = Color.White.WithAlpha( MathX.Clamp( left / FadeTime, 0, 1 ) );
		}
		while ( All.Count > 0 && (All.Peek().Born > Life || !All.Peek().Go.IsValid()) )
			Remove( All.Dequeue() );
	}

	static void Remove( Segment s )
	{
		if ( s.Go.IsValid() )
			s.Go.Destroy();
	}

	static Model Quad() => FlatQuad.Get( "materials/horizon/trace_pneu.vmat" );
}