Player/Ghost/GhostManager.cs

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.

File Access
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();
	}
}