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" );
}