Draws the local player's GPS route as a flat ribbon of vector points on the ground ahead of them. Rebuilds a LineRenderer every time the route changes or every half second, tracing each point down from above the player to sit on the road surface.
namespace HorizonRP;
/// <summary>
/// Le tracé du GPS dessiné au sol, devant le joueur (Julien, 29/10/2026) : un ruban bleu couché sur
/// la chaussée, qui suit l'itinéraire (<see cref="Gps.Route"/>) sur ses <see cref="Ahead"/> premiers
/// mètres. Créé chez le joueur seulement (NotNetworked) : personne d'autre ne le voit.
///
/// L'itinéraire est à plat (la carte ne sait pas les hauteurs) : le ruban est recoupé tous les
/// <see cref="Step"/> pouces et chaque point est posé au sol par un rayon vers le bas, lancé un
/// peu au-dessus du joueur — sous un pont du métro aérien, il trouve la route et pas le pont.
/// </summary>
public static class GpsTrail
{
/// <summary>Jusqu'où le tracé se dessine devant le joueur (mètres).</summary>
public static float Ahead { get; set; } = 160f;
public static float Width { get; set; } = 30f;
public static float Lift { get; set; } = 2f;
const float Step = 90f;
const float InchesToMeters = 0.0254f;
static GameObject trail;
static LineRenderer line;
static int builtVersion = -1;
static RealTimeSince sinceBuild;
/// <summary>Chaque image, chez le joueur local (Gps.Update).</summary>
public static void Update( RoleplayPlayer me )
{
if ( me is null || Gps.Target is null || Gps.Route.Count < 2 || Application.IsDedicatedServer )
{
Clear();
return;
}
if ( !trail.IsValid() )
Create();
// Retracé quand l'itinéraire change (recalcul, segment dépassé), et de temps en temps pour
// suivre le joueur qui avance sur un long segment.
if ( builtVersion != Gps.Version || sinceBuild > 0.5f )
Build( me );
}
public static void Clear()
{
if ( trail.IsValid() )
trail.Destroy();
trail = null;
line = null;
builtVersion = -1;
}
static void Create()
{
trail = new GameObject( false, "Tracé du GPS" );
trail.Flags |= GameObjectFlags.NotNetworked | GameObjectFlags.NotSaved;
line = trail.Components.Create<LineRenderer>();
line.UseVectorPoints = true;
line.VectorPoints = new();
line.Face = SceneLineObject.FaceMode.Normal;
line.AutoCalculateNormals = false;
line.Lighting = false;
line.CastShadows = false;
line.Opaque = false;
line.Additive = false;
line.Width = Width;
var color = new Gradient( new Gradient.ColorFrame( 0, new Color( 0.1f, 0.55f, 1f, 0.75f ) ), new Gradient.ColorFrame( 1, new Color( 0.1f, 0.55f, 1f, 0.75f ) ) );
line.Color = color;
trail.Enabled = true;
}
static void Build( RoleplayPlayer me )
{
builtVersion = Gps.Version;
sinceBuild = 0;
var route = Gps.Route;
var feet = me.WorldPosition;
var points = new List<Vector3>();
var left = Ahead / InchesToMeters;
for ( var k = 0; k < route.Count - 1 && left > 0; k++ )
{
var a = route[k].WithZ( 0 );
var b = route[k + 1].WithZ( 0 );
var length = a.Distance( b );
var steps = Math.Max( 1, (int)MathF.Ceiling( MathF.Min( length, left ) / Step ) );
for ( var s = k == 0 ? 0 : 1; s <= steps; s++ )
{
var t = MathF.Min( s * Step, MathF.Min( length, left ) ) / MathF.Max( 1, length );
points.Add( Ground( me, Vector3.Lerp( a, b, t ), feet.z ) );
}
left -= length;
}
if ( points.Count < 2 )
{
line.VectorPoints = new();
return;
}
line.VectorPoints = points;
}
/// <summary>Le sol sous un point du tracé, cherché depuis un peu au-dessus des pieds du joueur.</summary>
static Vector3 Ground( RoleplayPlayer me, Vector3 at, float feetZ )
{
var from = at.WithZ( feetZ + 120 );
var hit = me.Scene.Trace.Ray( from, from + Vector3.Down * 600 )
.IgnoreGameObjectHierarchy( me.GameObject )
.WithoutTags( "vehicule", "trigger", "player" )
.Run();
return (hit.Hit ? hit.EndPosition : at.WithZ( feetZ )) + Vector3.Up * Lift;
}
}