Wave Spawner
Wave-based enemy spawner with configurable wave definitions, spawn points, delays, and wave completion callbacks.
How to Use
Create empty GameObjects as spawn points
Attach WaveSpawner to a manager object
Define waves: each wave has enemy prefabs and counts
Assign spawn points array
Hook OnWaveStart/OnWaveComplete to your UI
Enemies are tracked automatically (destroy them to advance)
OnAllWavesComplete fires when final wave is cleared
Features
- Configurable wave definitions with multiple enemy types per wave
- Multiple spawn points with automatic round-robin distribution
- Adjustable delay between spawns and between waves
- Automatic alive-enemy tracking with cleanup of destroyed objects
- UnityEvents for wave start, wave complete, all waves complete, and enemy count changes
- SkipToNextWave method to force-advance for testing or power-ups
When to Use This
Perfect for tower defense, arena survival, horde modes, and any wave-based combat game. Use in FPS survival maps, RTS defense scenarios, or mobile arcade games. Combine with Object Pool for better performance on mobile when spawning many enemies.
Common Mistakes
You must assign at least one spawn point Transform in the Inspector — the spawner logs an error and stops if spawnPoints is empty. Enemies are tracked by GameObject reference, so they must be Destroyed (not just disabled) for the wave to advance. If waves aren't progressing, check that enemy prefabs are actually being destroyed when killed.
Source Code
using UnityEngine;
using UnityEngine.Events;
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// Wave-based enemy spawner. Define waves with enemy types,
/// counts, and spawn delays. Tracks alive enemies per wave.
/// </summary>
public class WaveSpawner : MonoBehaviour
{
[System.Serializable]
public class Wave
{
public string waveName = "Wave";
public EnemySpawn[] enemies;
public float delayBetweenSpawns = 0.5f;
}
[System.Serializable]
public class EnemySpawn
{
public GameObject prefab;
public int count = 5;
}
[Header("Waves")]
[SerializeField] private Wave[] waves;
[SerializeField] private float delayBetweenWaves = 5f;
[SerializeField] private bool autoStart = true;
[Header("Spawn Points")]
[SerializeField] private Transform[] spawnPoints;
[Header("Events")]
public UnityEvent<int> OnWaveStart; // wave index
public UnityEvent<int> OnWaveComplete; // wave index
public UnityEvent OnAllWavesComplete;
public UnityEvent<int, int> OnEnemyCountChanged; // alive, total
private int currentWaveIndex;
private List<GameObject> aliveEnemies = new List<GameObject>();
private int totalEnemiesInWave;
private bool isSpawning;
/// <summary>Current wave number (1-based).</summary>
public int CurrentWave => currentWaveIndex + 1;
/// <summary>Total number of waves.</summary>
public int TotalWaves => waves != null ? waves.Length : 0;
/// <summary>Is the spawner currently active?</summary>
public bool IsActive => isSpawning;
/// <summary>Number of enemies still alive.</summary>
public int AliveCount => aliveEnemies.Count;
private void Start()
{
if (autoStart && waves != null && waves.Length > 0)
StartWaves();
}
/// <summary>Begin the wave sequence.</summary>
public void StartWaves()
{
if (spawnPoints == null || spawnPoints.Length == 0)
{
Debug.LogError("[WaveSpawner] No spawn points assigned!");
return;
}
currentWaveIndex = 0;
StartCoroutine(SpawnWave());
}
private IEnumerator SpawnWave()
{
if (currentWaveIndex >= waves.Length)
{
OnAllWavesComplete?.Invoke();
yield break;
}
isSpawning = true;
Wave wave = waves[currentWaveIndex];
OnWaveStart?.Invoke(currentWaveIndex);
aliveEnemies.Clear();
totalEnemiesInWave = 0;
foreach (var enemySpawn in wave.enemies)
totalEnemiesInWave += enemySpawn.count;
int spawnIndex = 0;
foreach (var enemySpawn in wave.enemies)
{
for (int i = 0; i < enemySpawn.count; i++)
{
Transform spawnPoint = spawnPoints[spawnIndex % spawnPoints.Length];
spawnIndex++;
Vector3 offset = new Vector3(Random.Range(-1f, 1f), 0f, Random.Range(-1f, 1f));
GameObject enemy = Instantiate(
enemySpawn.prefab,
spawnPoint.position + offset,
spawnPoint.rotation
);
aliveEnemies.Add(enemy);
OnEnemyCountChanged?.Invoke(aliveEnemies.Count, totalEnemiesInWave);
yield return new WaitForSeconds(wave.delayBetweenSpawns);
}
}
isSpawning = false;
// Wait for all enemies to be destroyed
yield return StartCoroutine(WaitForWaveClear());
OnWaveComplete?.Invoke(currentWaveIndex);
currentWaveIndex++;
if (currentWaveIndex < waves.Length)
{
yield return new WaitForSeconds(delayBetweenWaves);
StartCoroutine(SpawnWave());
}
else
{
OnAllWavesComplete?.Invoke();
}
}
private IEnumerator WaitForWaveClear()
{
while (true)
{
aliveEnemies.RemoveAll(e => e == null);
OnEnemyCountChanged?.Invoke(aliveEnemies.Count, totalEnemiesInWave);
if (aliveEnemies.Count == 0)
yield break;
yield return new WaitForSeconds(0.5f);
}
}
/// <summary>Skip to the next wave immediately.</summary>
public void SkipToNextWave()
{
foreach (var enemy in aliveEnemies)
{
if (enemy != null) Destroy(enemy);
}
aliveEnemies.Clear();
}
}