Part of these game systems:
intermediate Utilities

Wave Spawner

Wave-based enemy spawner with configurable wave definitions, spawn points, delays, and wave completion callbacks.

Unity 2022.3+ · 3.0 KB · WaveSpawner.cs

How to Use

1

Create empty GameObjects as spawn points

2

Attach WaveSpawner to a manager object

3

Define waves: each wave has enemy prefabs and counts

4

Assign spawn points array

5

Hook OnWaveStart/OnWaveComplete to your UI

6

Enemies are tracked automatically (destroy them to advance)

7

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

WaveSpawner.cs
C#
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();
    }
}