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

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