Component on a player car that handles non-destructive respawning. It records the last grounded position, computes a safe respawn transform (using the racing line, spawn points, checkpoints), hides/disables the car for a brief period, repositions it, restores physics and visuals, and spawns sound/visual effects.
using Machines.Components;
using Machines.Events;
using Machines.Race;
using Machines.Systems;
namespace Machines.Player;
/// <summary>
/// Non-destructive respawn: disables the car briefly, repositions it on the racing line, re-enables it
/// </summary>
public sealed class CarRespawn : Component
{
[RequireComponent]
public Car Car { get; private set; }
/// <summary>
/// How long the car stays hidden/disabled before reappearing on the track.
/// </summary>
[Property]
public float RespawnHideTime { get; set; } = 3f;
/// <summary>
/// Minimum time between respawns (debounces overlapping kill-volume triggers).
/// </summary>
[Property]
public float RespawnDebounce { get; set; } = 1.5f;
/// <summary>
/// How far above the surface the car is placed on respawn so it settles cleanly.
/// </summary>
[Property]
public float SpawnLift { get; set; } = 16f;
/// <summary>
/// Step size backward along the racing line when a respawn spot is blocked.
/// </summary>
[Property]
public float ClearSpotStep { get; set; } = 80f;
/// <summary>
/// How many backward steps to try before giving up and using the blocked spot anyway.
/// </summary>
[Property]
public int ClearSpotAttempts { get; set; } = 8;
/// <summary>
/// Prefab to spawn as a visual effect when the car reappears after respawn.
/// </summary>
[Property]
public GameObject RespawnEffectPrefab { get; set; }
/// <summary>
/// Sound event to play when the car reappears after respawn.
/// </summary>
[Property]
public SoundEvent RespawnSound { get; set; }
/// <summary>
/// True while hidden/disabled during respawn, synced to all clients.
/// </summary>
[Sync]
public bool IsRespawning { get; private set; }
// Owner-only: last grounded transform and cached racing-line projection.
private Vector3 _lastGroundedPos;
private float _lastGroundedYaw;
private bool _hasGrounded;
private float _lastGroundedPathDistance = -1f;
private float _reenableAt;
private float _nextRespawnAllowed;
// All machines: idempotency + cached components/objects.
private bool _appliedState;
private Rigidbody _rb;
private Collider _collider;
private GameObject _meshObject;
private bool IsAuthority => Car.IsValid() && Car.IsAuthority;
protected override void OnStart()
{
_rb = GetComponent<Rigidbody>();
_collider = GetComponent<Collider>();
_meshObject = Car.Renderer.IsValid() ? Car.Renderer.GameObject : null;
}
protected override void OnFixedUpdate()
{
if ( !IsAuthority || IsRespawning || !Car.Movement.IsValid() )
return;
// Track last grounded position; no-respawn zones are skipped.
if ( Car.Movement.IsGrounded && !InNoRespawnZone( WorldPosition ) )
{
_lastGroundedPos = WorldPosition;
_lastGroundedYaw = Car.Movement.Yaw;
_hasGrounded = true;
var line = RacingPath.Current?.Optimal;
if ( line is not null && line.IsValid )
_lastGroundedPathDistance = line.GetDistanceAtPosition( _lastGroundedPos, _lastGroundedPathDistance, 1500f );
}
}
protected override void OnUpdate()
{
ApplyState();
if ( IsAuthority && IsRespawning && Time.Now >= _reenableAt )
{
IsRespawning = false;
ApplyState();
// Restore music volume for local player
if ( Car.IsLocalPlayer )
MusicManager.Unduck( fadeUp: 1.5f );
// Nudge bot AI to re-sync to the new position.
GetComponent<BotBrain>()?.OnRespawned();
// Visual/audio feedback + notify other systems.
SpawnRespawnEffect();
Scene.RunEvent<ICarRespawnListener>( x => x.OnCarRespawned( Car ) );
}
}
/// <summary>
/// Begin a respawn. Owner-authoritative; a no-op on proxies or while already respawning.
/// </summary>
public void Respawn()
{
if ( !IsAuthority || IsRespawning || Time.Now < _nextRespawnAllowed )
return;
_nextRespawnAllowed = Time.Now + RespawnDebounce;
IsRespawning = true;
ApplyState();
// Duck music while the local player is dead
if ( Car.IsLocalPlayer )
MusicManager.Duck( 0.2f, fadeDown: 0.3f );
var (pos, rot) = ComputeRespawnTransform();
Car.Movement?.ResetMotion();
WorldPosition = pos;
WorldRotation = rot;
Car.Movement?.SetYaw( rot.Yaw() );
_reenableAt = Time.Now + RespawnHideTime;
}
private (Vector3 pos, Rotation rot) ComputeRespawnTransform()
{
static Rotation AlongLine( RacingLine line, float distance )
=> Rotation.LookAt( line.GetTangentAtDistance( distance ), Vector3.Up );
// Primary: project last grounded position onto the optimal racing line.
var line = RacingPath.Current?.Optimal;
if ( _hasGrounded && line is not null && line.IsValid )
{
var d = _lastGroundedPathDistance >= 0f
? line.GetDistanceAtPosition( _lastGroundedPos, _lastGroundedPathDistance, 1500f )
: line.GetDistanceAtPosition( _lastGroundedPos );
// Step backward until spot is clear; never forward (no progress grants).
for ( int i = 0; i <= ClearSpotAttempts; i++ )
{
var cd = d - i * ClearSpotStep;
if ( TryFindClearSpot( line.GetPointAtDistance( cd ), out var clear ) )
return (clear, AlongLine( line, cd ));
}
// Nothing clear: fall back to the projected point.
return (SnapToSurface( line.GetPointAtDistance( d ) ), AlongLine( line, d ));
}
// Fallback 1: last grounded transform (was on the surface).
if ( _hasGrounded && TryFindClearSpot( _lastGroundedPos, out var grounded ) )
return (grounded, Rotation.FromYaw( _lastGroundedYaw ));
// Fallback 2: authored spawn point (no surface snap).
var slot = Car.Slot >= 0 ? Car.Slot : 0;
var sp = Machines.Components.SpawnPoint.ForSlot( slot );
if ( sp.IsValid() )
return (sp.WorldPosition, sp.WorldRotation);
// Fallback 3: nearest clear checkpoint, else nearest regardless.
Checkpoint nearest = null;
foreach ( var cp in Scene.GetAll<Checkpoint>().OrderBy( c => (c.WorldPosition - WorldPosition).LengthSquared ) )
{
nearest ??= cp;
if ( TryFindClearSpot( cp.WorldPosition, out var clear ) )
return (clear, cp.WorldRotation);
}
if ( nearest.IsValid() )
return (SnapToSurface( nearest.WorldPosition ), nearest.WorldRotation);
// Last resort: last grounded (even if blocked), else stay put.
if ( _hasGrounded )
return (SnapToSurface( _lastGroundedPos ), Rotation.FromYaw( _lastGroundedYaw ));
return (WorldPosition, WorldRotation);
}
/// <summary>
/// Snap a candidate to the ground and validate the car can actually stand there.
/// </summary>
private bool TryFindClearSpot( Vector3 basePos, out Vector3 pos )
{
return TrySnapToSurface( basePos, out pos ) && IsSpotClear( pos );
}
private bool InNoRespawnZone( Vector3 pos )
{
return Scene.GetAll<NoRespawnZone>().Any( z => z.IsValid() && z.Contains( pos ) );
}
/// <summary>
/// True if the car can stand at the given surface-snapped position: not inside geometry or another car.
/// </summary>
private bool IsSpotClear( Vector3 pos )
{
if ( InNoRespawnZone( pos ) )
return false;
var movement = Car.Movement;
if ( !movement.IsValid() )
return true;
var radius = movement.Radius;
// Overlap the same sphere the mover uses so "clear" matches collision.
var center = pos + Vector3.Up * (radius - movement.HoverHeight + movement.GroundClearance);
var tr = Scene.Trace.Sphere( radius, center, center )
.IgnoreGameObjectHierarchy( GameObject )
.WithCollisionRules( "player" )
.Run();
if ( tr.Hit || tr.StartedSolid )
return false;
// Spot must be on the navmesh (drivable track); scenes without a navmesh skip this check.
var nav = Scene.NavMesh;
if ( nav is not null && nav.IsEnabled )
{
var ground = pos - Vector3.Up * SpawnLift;
var onMesh = nav.GetClosestPoint( ground );
if ( onMesh is null )
return false;
// Reject if nearest mesh point is off to the side or on a different floor.
if ( (onMesh.Value - ground).WithZ( 0f ).Length > radius )
return false;
if ( MathF.Abs( onMesh.Value.z - ground.z ) > 64f )
return false;
}
// Player-player rule is Trigger, so check cars by distance manually.
foreach ( var other in Scene.GetAll<Car>() )
{
if ( !other.IsValid() || other == Car || !other.IsPhysicallyPresent )
continue;
var otherRadius = other.Movement.IsValid() ? other.Movement.Radius : radius;
var minDist = radius + otherRadius;
if ( MathF.Abs( pos.z - other.WorldPosition.z ) > minDist )
continue;
if ( (pos - other.WorldPosition).WithZ( 0f ).Length < minDist )
return false;
}
return true;
}
/// <summary>
/// Trace down to find ground and lift the car above it; falls back to a flat offset if nothing walkable is found.
/// </summary>
private Vector3 SnapToSurface( Vector3 basePos )
{
TrySnapToSurface( basePos, out var pos );
return pos;
}
/// <summary>
/// Like <see cref="SnapToSurface"/> but returns false when falling back to the flat offset.
/// </summary>
private bool TrySnapToSurface( Vector3 basePos, out Vector3 pos )
{
var tr = Scene.Trace.Ray( basePos + Vector3.Up * 30f, basePos + Vector3.Down * 80f )
.WithoutTags( "player", "car" )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
if ( !tr.Hit || tr.Normal.z < 0.5f )
{
pos = basePos + Vector3.Up * SpawnLift;
return false;
}
pos = tr.HitPosition + Vector3.Up * SpawnLift;
return true;
}
[Rpc.Broadcast( NetFlags.OwnerOnly )]
private void SpawnRespawnEffect()
{
if ( RespawnEffectPrefab.IsValid() )
{
RespawnEffectPrefab.Clone( WorldPosition );
}
if ( RespawnSound.IsValid() )
Sound.Play( RespawnSound, WorldPosition );
}
/// <summary>
/// Toggle components to match <see cref="IsRespawning"/>. Idempotent; visuals toggle everywhere, sim toggles owner-only.
/// </summary>
private void ApplyState()
{
if ( _appliedState == IsRespawning )
return;
_appliedState = IsRespawning;
if ( IsRespawning )
{
// Disable in order: physics -> sim -> visuals.
SetSimEnabled( false );
SetVisualsEnabled( false );
}
else
{
// Restore in order: visuals -> sim -> physics (physics last to settle on final transform).
SetVisualsEnabled( true );
SetSimEnabled( true );
if ( IsAuthority && _rb.IsValid() )
{
_rb.Velocity = Vector3.Zero;
_rb.AngularVelocity = Vector3.Zero;
}
if ( IsAuthority )
Car.Movement?.SetYaw( WorldRotation.Yaw() );
}
}
private void SetVisualsEnabled( bool on )
{
if ( _meshObject.IsValid() )
_meshObject.Enabled = on;
if ( Car.BodyTilt.IsValid() )
Car.BodyTilt.Enabled = on;
var wheels = GetComponent<CarWheelVisuals>();
if ( wheels.IsValid() )
wheels.Enabled = on;
}
private void SetSimEnabled( bool on )
{
if ( !IsAuthority )
return;
if ( Car.Movement.IsValid() ) Car.Movement.Enabled = on;
if ( Car.Input.IsValid() ) Car.Input.Enabled = on;
if ( Car.Drift.IsValid() ) Car.Drift.Enabled = on;
if ( Car.Boost.IsValid() ) Car.Boost.Enabled = on;
if ( Car.Collision.IsValid() ) Car.Collision.Enabled = on;
// Collider toggled so the hidden car can't be hit; rigidbody stays kinematic (mover drives it).
if ( _collider.IsValid() ) _collider.Enabled = on;
}
}