U
tab 04World & Game Loop7 lessons — tap to open
06 / 07 · 12 min
World & Game Loop

Waves, losing and starting again

your project so far

Assets/

Scenes/

Arena.unity

VictoryPanelDefeatPanelSpawnPoint ×4

Scripts/

CharacterStats.OnDied eventHealthBar.csGameDirector.csEndPanels.cs

Prefabs/

Enemy.prefab

dressed arena · waves spawned by GameDirector

you already havethis lesson addsthis lesson retires

Everything works. The Knight jumps the blocks, three skills land, enemies chase and hurt you, and the arena is lit, has sound and runs fast. Press Play and you can do all of it forever, because nothing ever ends.

A demo lets you do things. A game tells you when you have won and when you have lost. That difference is one enum and about forty lines.

animated diagram

Three waves, then one of two endings — and a way back to the start from either.

Step 1 — one enum, one place

Scatter the state across scripts and you will spend an evening asking why the defeat screen shows during a victory. Keep it in one object and every other script just reads it.

GameDirector.cs
using System.Collections;
using UnityEngine;

public enum GameState { Ready, Fighting, Cleared, Defeat }

public class GameDirector : MonoBehaviour
{
    public static GameDirector I;              // one director, reachable from anywhere

    public GameState State { get; private set; } = GameState.Ready;

    public Transform[] spawnPoints;
    public GameObject enemyPrefab;
    public CharacterStats player;
    public int[] waveSizes = { 2, 3, 4 };

    int alive;

    void Awake() => I = this;

    void Start()
    {
        player.OnDied += () => Finish(GameState.Defeat);
        StartCoroutine(RunWaves());
    }

    IEnumerator RunWaves()
    {
        State = GameState.Fighting;

        foreach (int size in waveSizes)
        {
            alive = size;
            for (int i = 0; i < size; i++)
            {
                Transform where = spawnPoints[Random.Range(0, spawnPoints.Length)];
                GameObject e = Instantiate(enemyPrefab, where.position, where.rotation);
                e.GetComponent<CharacterStats>().OnDied += () => alive--;
            }

            // wait until the player clears this wave, or dies trying
            while (alive > 0 && State == GameState.Fighting) yield return null;
            if (State != GameState.Fighting) yield break;

            yield return new WaitForSeconds(2f);   // a breath between waves
        }

        Finish(GameState.Cleared);
    }

    void Finish(GameState ending)
    {
        State = ending;
        Time.timeScale = 0f;                        // freeze the arena behind the panel
    }

    public void Restart()
    {
        Time.timeScale = 1f;
        UnityEngine.SceneManagement.SceneManager.LoadScene(
            UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex);
    }
}
  • OnDied is the event you added to CharacterStats in tab 02, along with the health bar. The director never searches the scene for enemies — each enemy tells it when it goes.

  • Time.timeScale = 0f stops everything at once: enemies, animations, particles. You do not have to disable them one by one.

  • Reloading the scene is the cheapest possible restart and it can never leave stale state behind — the whole arena is rebuilt from the file.

Step 2 — the two panels

  1. 1

    Inside the Canvas from tab 01 — the one that holds HudPanel and PausePanel — add two more panels: VictoryPanel and DefeatPanel, each with a title and one Button.

  2. 2

    Set both inactive in the Inspector. They only exist when the run is over.

  3. 3

    Wire each Button's OnClick to GameDirector.Restart.

  4. 4

    Add a small script that watches GameDirector.I.State and shows the matching panel.

EndPanels.cs
using UnityEngine;

public class EndPanels : MonoBehaviour
{
    public GameObject victory;
    public GameObject defeat;

    void Update()
    {
        // Update still runs at timeScale 0 — that is why the panel can appear.
        victory.SetActive(GameDirector.I.State == GameState.Cleared);
        defeat.SetActive(GameDirector.I.State == GameState.Defeat);
    }
}