Editor/debug partial for RacingPath that draws debug gizmos for optimal/left/right racing lines, path segments, hints, camera spots and shortcuts. It toggles drawing via ConVar booleans and uses Gizmo drawing API to render lines, labels, arrows and spheres.
namespace Machines.Race;
/// <summary>
/// Debug gizmo drawing for <see cref="RacingPath"/>, toggled via <c>race_debug_*</c> convars.
/// </summary>
public sealed partial class RacingPath
{
[ConVar( "race_debug_paths" )]
public static bool DebugPaths { get; set; } = false;
[ConVar( "race_debug_segments" )]
public static bool DebugSegments { get; set; } = false;
[ConVar( "race_debug_hints" )]
public static bool DebugHints { get; set; } = false;
[ConVar( "race_debug_cameras" )]
public static bool DebugCameras { get; set; } = false;
[ConVar( "race_debug_shortcuts" )]
public static bool DebugShortcuts { get; set; } = false;
protected override void DrawGizmos()
{
if ( !DebugPaths && !DebugSegments && !DebugHints && !DebugCameras && !DebugShortcuts )
return;
Gizmo.Transform = global::Transform.Zero;
if ( DebugPaths )
{
DrawLine( Optimal, Color.Green );
DrawLine( Left, new Color( 0.3f, 0.6f, 1f ) );
DrawLine( Right, new Color( 1f, 0.4f, 0.2f ) );
}
if ( DebugSegments )
{
DrawSegments();
}
if ( DebugHints )
{
DrawHints();
}
if ( DebugCameras )
{
DrawCameraSpots();
}
if ( DebugShortcuts )
{
DrawShortcuts();
}
}
/// <summary>
/// Draw the optimal line color-coded by segment type, with boundary labels.
/// </summary>
private void DrawSegments()
{
if ( Optimal == null || !Optimal.IsValid || Optimal.Segments.Count == 0 )
return;
const int subdivisions = 6;
const float labelHeight = 40f;
const float dashLength = 30f;
for ( int i = 0; i < Optimal.Points.Count; i++ )
{
var dist = i < Optimal.CumulativeDistances.Count ? Optimal.CumulativeDistances[i] : 0f;
var seg = Optimal.GetSegmentAtDistance( dist );
var color = GetSegmentColor( seg );
Gizmo.Draw.Color = color;
Gizmo.Draw.LineThickness = seg?.IsGap == true ? 1f : 2f;
var prev = TrackSpline.GetPoint( Optimal.Points, i, 0f );
for ( int s = 1; s <= subdivisions; s++ )
{
var t = s / (float)subdivisions;
var curr = TrackSpline.GetPoint( Optimal.Points, i, t );
// For gaps, draw dashed: skip every other sub-segment
if ( seg?.IsGap == true )
{
var subDist = dist + (Optimal.CumulativeDistances[Math.Min( i + 1, Optimal.CumulativeDistances.Count - 1 )] - dist) * t;
var phase = (subDist % dashLength) / dashLength;
if ( phase < 0.5f )
Gizmo.Draw.Line( prev, curr );
}
else
{
Gizmo.Draw.Line( prev, curr );
}
prev = curr;
}
}
// Labels at segment boundaries
foreach ( var seg in Optimal.Segments )
{
var pos = Optimal.GetPointAtDistance( seg.StartDistance );
var label = seg.IsGap ? "GAP" : $"{seg.Type}";
if ( !string.IsNullOrEmpty( seg.SourceLabel ) && !seg.IsGap )
label += $" ({seg.SourceLabel})";
Gizmo.Draw.Color = GetSegmentColor( seg );
Gizmo.Draw.Text( label, new Transform( pos + Vector3.Up * labelHeight ) );
// Vertical marker at boundary
Gizmo.Draw.Line( pos, pos + Vector3.Up * labelHeight * 0.8f );
}
}
private static Color GetSegmentColor( PathSegmentInfo seg )
{
if ( seg == null )
return Color.Gray;
if ( seg.IsGap )
return new Color( 1f, 0.2f, 0.2f ); // Red
return seg.Type switch
{
SplineType.Road => new Color( 0.2f, 0.9f, 0.3f ), // Green
SplineType.Bridge => new Color( 0.2f, 0.85f, 0.9f ), // Cyan
SplineType.OffRoad => new Color( 0.9f, 0.75f, 0.2f ), // Yellow
SplineType.Tunnel => new Color( 0.7f, 0.3f, 0.9f ), // Purple
_ => Color.White
};
}
private void DrawLine( RacingLine line, Color color )
{
if ( line == null || !line.IsValid )
return;
Gizmo.Draw.Color = color;
const int subdivisions = 6;
for ( int i = 0; i < line.Points.Count; i++ )
{
var prev = TrackSpline.GetPoint( line.Points, i, 0f );
for ( int s = 1; s <= subdivisions; s++ )
{
var t = s / (float)subdivisions;
var curr = TrackSpline.GetPoint( line.Points, i, t );
Gizmo.Draw.Line( prev, curr );
prev = curr;
}
// Direction arrow every 10 waypoints
if ( i % 10 == 0 )
{
var pos = line.Points[i];
var nextIdx = (i + 1) % line.Points.Count;
var dir = (line.Points[nextIdx] - pos).Normal;
var arrowEnd = pos + dir * 20f;
Gizmo.Draw.Arrow( pos + Vector3.Up * 10f, arrowEnd + Vector3.Up * 10f, 4f, 2f );
}
}
}
/// <summary>
/// Draw path hints as color-coded vertical bars (green=grip, yellow=reduced, red=low/airborne).
/// </summary>
private void DrawHints()
{
if ( Optimal == null || !Optimal.IsValid || Optimal.Hints.Count == 0 )
return;
const float barHeight = 30f;
var step = RacingLine.CurvatureStep;
for ( int i = 0; i < Optimal.Hints.Count; i++ )
{
var hint = Optimal.Hints[i];
var d = i * step;
var pos = Optimal.GetPointAtDistance( d );
// Friction: green (1.0) -> yellow (0.5) -> red (0.0)
var color = hint.Friction > 0.7f
? Color.Lerp( new Color( 1f, 1f, 0f ), new Color( 0.2f, 1f, 0.3f ), (hint.Friction - 0.7f) / 0.3f )
: Color.Lerp( new Color( 1f, 0.1f, 0.1f ), new Color( 1f, 1f, 0f ), hint.Friction / 0.7f );
if ( hint.Flags.HasFlag( PathHintFlags.Airborne ) )
color = new Color( 0.5f, 0.5f, 1f ); // Blue for airborne
if ( hint.Flags.HasFlag( PathHintFlags.Corner ) )
color = new Color( 1f, 0.6f, 0.1f ); // Orange for corners
Gizmo.Draw.Color = color;
// Bar height scales with friction
var height = barHeight * MathF.Max( 0.2f, hint.Friction );
Gizmo.Draw.Line( pos, pos + Vector3.Up * height );
// Flag labels at notable points (skip every-sample to reduce noise)
if ( hint.Flags != PathHintFlags.None && i % 3 == 0 )
{
var label = "";
if ( hint.Flags.HasFlag( PathHintFlags.LowGrip ) ) label += "LOW ";
if ( hint.Flags.HasFlag( PathHintFlags.Airborne ) ) label += "AIR ";
if ( hint.Flags.HasFlag( PathHintFlags.Narrow ) ) label += "NAR ";
if ( hint.Flags.HasFlag( PathHintFlags.SlowZone ) ) label += "SLOW ";
Gizmo.Draw.Text( label.Trim(), new Transform( pos + Vector3.Up * (barHeight + 10f) ) );
}
}
}
/// <summary>
/// Draw baked camera spots as spheres with lines to the track point they watch.
/// </summary>
private void DrawCameraSpots()
{
if ( CameraSpots.Count == 0 )
return;
for ( int i = 0; i < CameraSpots.Count; i++ )
{
var spot = CameraSpots[i];
var trackPoint = Optimal.GetPointAtDistance( spot.Distance );
// Camera sphere
Gizmo.Draw.Color = new Color( 1f, 0.85f, 0.1f ); // Gold
Gizmo.Draw.LineSphere( spot.Position, 8f );
// Line to track point
Gizmo.Draw.Color = new Color( 1f, 0.85f, 0.1f, 0.4f );
Gizmo.Draw.Line( spot.Position, trackPoint );
// Label
Gizmo.Draw.Color = Color.White;
Gizmo.Draw.Text( $"CAM {i}", new Transform( spot.Position + Vector3.Up * 12f ) );
}
}
/// <summary>
/// Draw baked shortcut paths with entry/exit markers.
/// </summary>
private void DrawShortcuts()
{
if ( Shortcuts.Count == 0 || Optimal == null || !Optimal.IsValid )
return;
for ( int s = 0; s < Shortcuts.Count; s++ )
{
var shortcut = Shortcuts[s];
var color = new Color( 1f, 0.2f, 1f ); // Magenta
// Shortcut path
Gizmo.Draw.Color = color;
Gizmo.Draw.LineThickness = 3f;
for ( int i = 0; i < shortcut.Points.Count - 1; i++ )
Gizmo.Draw.Line( shortcut.Points[i], shortcut.Points[i + 1] );
// Waypoint spheres
Gizmo.Draw.Color = color.WithAlpha( 0.6f );
foreach ( var pt in shortcut.Points )
Gizmo.Draw.LineSphere( pt, 4f );
// Entry marker
var entryPoint = Optimal.GetPointAtDistance( shortcut.EntryDistance );
Gizmo.Draw.Color = new Color( 0.2f, 1f, 0.2f );
Gizmo.Draw.LineSphere( shortcut.Points[0], 10f );
Gizmo.Draw.Line( shortcut.Points[0], entryPoint );
Gizmo.Draw.Text( $"SC{s} ENTRY", new Transform( shortcut.Points[0] + Vector3.Up * 20f ) );
// Exit marker
var exitPoint = Optimal.GetPointAtDistance( shortcut.ExitDistance );
Gizmo.Draw.Color = new Color( 1f, 0.3f, 0.2f );
Gizmo.Draw.LineSphere( shortcut.Points[^1], 10f );
Gizmo.Draw.Line( shortcut.Points[^1], exitPoint );
Gizmo.Draw.Text( $"SC{s} EXIT", new Transform( shortcut.Points[^1] + Vector3.Up * 20f ) );
// Direction arrows
Gizmo.Draw.Color = color;
for ( int i = 0; i < shortcut.Points.Count - 1; i += Math.Max( 1, shortcut.Points.Count / 5 ) )
{
var from = shortcut.Points[i];
var to = shortcut.Points[Math.Min( i + 1, shortcut.Points.Count - 1 )];
var dir = (to - from).Normal;
var mid = (from + to) * 0.5f;
Gizmo.Draw.Line( mid, mid + dir * 15f );
}
}
}
}