test/EnemySpawner.cs
public sealed class EnemySpawner : Component
{
[Property] public List<EnemyEntry> Enemies { get; set; } = new();
/// <summary>
/// Curve that controls spawn interval (in seconds) evaluated over normalized game time (0-1).
/// </summary>
[Property] public Curve SpawnInterval { get; set; }
/// <summary>
/// Total duration of the game in seconds. Defaults to 30 minutes.
/// </summary>
[Property] public float MaxTime { get; set; } = 1800f;
/// <summary>
/// Radius around this component's position to find a NavMesh spawn point.
/// </summary>
[Property] public float SpawnRadius { get; set; } = 500f;
private TimeSince TimeSinceLastSpawn { get; set; }
protected override void OnStart()
{
TimeSinceLastSpawn = 0f;
}
protected override void OnUpdate()
{
var normalizedTime = MathF.Min( (GameManager.Instance?.Timer ?? 0) / MaxTime, 1f );
var interval = SpawnInterval.Evaluate( normalizedTime );
if ( TimeSinceLastSpawn >= interval )
{
SpawnEnemy( normalizedTime );
TimeSinceLastSpawn = 0f;
}
}
private void SpawnEnemy( float normalizedTime )
{
var available = Enemies.Where( e => normalizedTime >= e.MinGameTimeNormalized ).ToList();
if ( available.Count == 0 )
return;
var totalWeight = available.Sum( e => e.SpawnWeight );
var roll = Random.Shared.Float( 0f, totalWeight );
float accumulated = 0f;
foreach ( var entry in available )
{
accumulated += entry.SpawnWeight;
if ( roll <= accumulated )
{
var spawnPos = Scene.NavMesh.GetRandomPoint( Transform.Position, SpawnRadius ) ?? Transform.Position;
entry.Prefab.Clone( spawnPos + Vector3.Up * 32f );
break;
}
}
}
protected override void DrawGizmos()
{
Gizmo.Draw.Color = Color.Orange.WithAlpha( 0.3f );
Gizmo.Draw.LineSphere( Vector3.Zero, SpawnRadius );
}
}
public class EnemyEntry
{
[Property] public GameObject Prefab { get; set; }
/// <summary>
/// Normalized game time (0-1) at which this enemy becomes available to spawn.
/// </summary>
[Property, Range( 0f, 1f )] public float MinGameTimeNormalized { get; set; }
[Property] public float SpawnWeight { get; set; } = 1f;
}