Component that spawns and manages ghost cars from recorded runs. It clones a ghost prefab, applies a car model and opacity, creates/configures a GhostPlayer component to play back the recording, and tracks spawned ghosts for later despawn.
using Machines.Resources;
namespace Machines.Ghost;
/// <summary>
/// Spawns and manages ghost cars from recordings.
/// </summary>
public sealed class GhostManager : Component
{
/// <summary>
/// Ghost car prefab (model renderer, no physics).
/// </summary>
public GameObject GhostPrefab => GameObject.GetPrefab( "prefabs/ghost_car.prefab" );
/// <summary>
/// Opacity for the ghost car material (0-1).
/// </summary>
[Property]
public float GhostOpacity { get; set; } = 0.4f;
private readonly List<GameObject> _ghosts = new();
/// <summary>
/// Spawns a ghost car from a recording and starts playback.
/// </summary>
public GhostPlayer SpawnGhost( GhostRecording recording, float lapTime = 0f )
{
if ( recording is null || !recording.IsValid )
{
return null;
}
if ( !GhostPrefab.IsValid() )
{
return null;
}
var ghostGo = GhostPrefab.Clone( new CloneConfig
{
Transform = new Transform( Vector3.Zero ),
StartEnabled = true,
Name = "Ghost"
} );
// Apply the car model from the recording.
if ( !string.IsNullOrEmpty( recording.CarResourcePath ) )
{
var carResource = ResourceLibrary.Get<CarResource>( recording.CarResourcePath );
if ( carResource != null && carResource.Model.IsValid() )
{
var renderer = ghostGo.GetComponent<SkinnedModelRenderer>()
?? ghostGo.Components.GetInDescendants<SkinnedModelRenderer>();
if ( renderer.IsValid() )
{
renderer.Model = carResource.Model;
renderer.Tint = Color.White.WithAlpha( GhostOpacity );
}
}
}
var player = ghostGo.Components.GetOrCreate<GhostPlayer>();
player.Recording = recording;
player.PlayerName = recording.PlayerName;
player.LapTime = lapTime;
player.Loop = true;
player.StartPlayback();
_ghosts.Add( ghostGo );
return player;
}
/// <summary>
/// Destroy all spawned ghosts.
/// </summary>
public void DespawnAll()
{
foreach ( var ghost in _ghosts )
{
if ( ghost.IsValid() )
ghost.Destroy();
}
_ghosts.Clear();
}
/// <summary>
/// Destroy a specific ghost.
/// </summary>
public void Despawn( GhostPlayer ghost )
{
if ( !ghost.IsValid() )
return;
_ghosts.Remove( ghost.GameObject );
ghost.GameObject.Destroy();
}
protected override void OnDestroy()
{
DespawnAll();
}
}