Skip to main content

Stage 9: Win, Lose, and Game State

Course progressStage 9 of 10
~50 min
Build

a castle gate with health, a lose screen, and waves that end in a win

Learn

how the GameManager owns game state and how enemies reaching the gate cost you

Ship

a game you can actually win or lose

The big idea

Right now slimes reach the gate and just disappear with a Console message. There are no stakes. A game needs state: a few facts about how things are going — how much gold, how healthy the gate is, whether the game is still running. When that state crosses a line (gate health hits zero, last enemy cleared), the game ends.

We keep that state in one place: the GameManager. It already owns gold; now it also owns the gate's health and whether the game is over. The gate doesn't decide anything itself — when an enemy reaches it, the enemy tells the GameManager "take some health," and the GameManager decides if that means a loss. One brain, one set of facts. That's why it's called a manager.

To stop the game, we flip on a panel (a win or lose screen) and freeze time with Time.timeScale. To win, the spawner counts its waves and, once every enemy is gone, reports victory.

New words
Game state
The handful of facts that describe how the game is going right now: gold, gate health, and whether it's over.
SetActive
Turns a GameObject on or off. We keep the win/lose panels off (inactive) and SetActive(true) the right one when the game ends.
Time.timeScale
How fast game time runs. 1 is normal; setting it to 0 freezes everything — perfect for a game-over screen.
gateHealth
How many hits your castle gate can take before you lose. Lives on the GameManager.
Before you start

Finish Stage 8 — you need a working GameManager (with gold and UpdateUI), the Enemy script, and the WaveSpawner.

Build it

Step 1 — Give the gate health (Pass 1)

First, teach the GameManager to hold gate health and lose it on demand. Add these fields and a DamageGate method that, for now, just logs and lowers the number so you can watch it work.

Add to the top of GameManager (near your existing gold field):

public int gateHealth = 5;
public Text healthText;

Make sure using UnityEngine.UI; is at the top of the file (you already use it for goldText). Then add this method, with a temporary log so Pass 1 is easy to verify:

public void DamageGate(int amount)
{
gateHealth -= amount;
Debug.Log("Gate hit! Health now " + gateHealth);
UpdateUI();
}

And add a line to your existing UpdateUI so the gate health shows on screen:

if (healthText) healthText.text = "Gate: " + gateHealth;

Press Play and, just to test, call DamageGate(1) from anywhere (or wait until Step 2 wires it to enemies). The number drops and the UI updates. Stop.

Step 2 — Lose when the gate falls (Pass 2)

Now make a real loss. Add the panels, the game-over flag, and the win/lose methods to GameManager.

Script anatomy

The full script, line by line

The finished GameManager additions: it owns gate health, a game-over flag, and the two ways the game can end.

public int gateHealth = 5;
public Text healthText;
public GameObject winPanel;
public GameObject losePanel;
public int enemiesAlive = 0;
bool gameOver = false;

public void DamageGate(int amount)
{
gateHealth -= amount;
UpdateUI();
if (gateHealth <= 0) LoseGame();
}

public void WinGame()
{
if (gameOver) return;
gameOver = true;
if (winPanel) winPanel.SetActive(true);
Time.timeScale = 0f;
}

public void LoseGame()
{
if (gameOver) return;
gameOver = true;
if (losePanel) losePanel.SetActive(true);
Time.timeScale = 0f;
}
  1. Lines 5–6enemiesAlive and the game-over guard

    enemiesAlive counts living enemies (the spawner needs this to know the wave is cleared). gameOver makes sure we only end the game once — no double win/lose.

  2. Lines 8–13DamageGate decides the loss

    Each hit lowers health and refreshes the UI. The moment health reaches zero or less, it calls LoseGame. The gate object itself holds no logic — the manager does.

  3. Lines 15–21WinGame / LoseGame are twins

    Both check the guard first, flip it on, switch on the right panel with SetActive(true), then freeze the world with Time.timeScale = 0. SetActive shows a panel you kept hidden; timeScale 0 stops all motion and timers.

Now teach the enemy to actually hit the gate and to keep the alive-count honest. In Enemy.cs, add a field and two lifecycle methods:

public int damageToGate = 1;

void Start()
{
if (GameManager.instance) GameManager.instance.enemiesAlive++;
}

void OnDestroy()
{
if (GameManager.instance) GameManager.instance.enemiesAlive--;
}

Then, in Enemy.Update, replace the old Stage 1 Debug.Log at the gate with a real hit:

if (transform.position.x <= gateX)
{
if (GameManager.instance) GameManager.instance.DamageGate(damageToGate);
Destroy(gameObject);
}
Build this GameObject

Castle Gate

2D Sprite
Open recipe
Sprite
a door/gate sprite from your sliced tilemap
Position
(-8, 0, 0) — the left edge, at gateX
Order in Layer
2
Components to add
  • Sprite Renderer

The gate is just art that marks where gateX is. All the gate's logic lives on the GameManager, not on this object.

Build this GameObject

Lose Panel (UI)

UI > Panel
Open recipe
Sprite
a 'Gate Breached' / 'You Lose' label inside the panel
Active
UNCHECKED (inactive by default)
Wired into
GameManager → Lose Panel field
Components to add
  • Panel (Image)
  • Text child

Make the panel cover the screen, add a Text saying you lost, then UNCHECK the box at the top of its Inspector so it starts hidden. Drag it into the GameManager's Lose Panel slot.

Press Play and let a slime reach the gate. Gate health drops. Let enough through and the Lose Panel appears and everything freezes. That's a real loss.

Step 3 — Finite waves and a win (Pass 3)

A game you can only lose isn't a game. Let the player win by surviving a set number of waves. Add wave-counting to WaveSpawner.

Add these fields:

public int totalWaves = 5;
public int enemiesPerWave = 5;

Track how many enemies you've spawned, and stop spawning once you've sent them all. A simple way: count spawns in SpawnEnemy, and in Update, once you've spawned totalWaves * enemiesPerWave enemies and GameManager.instance.enemiesAlive <= 0, call WinGame exactly once.

Script anatomy

The full script, line by line

The WaveSpawner additions: spawn a finite number of enemies, then declare victory once the courtyard is empty.

public int totalWaves = 5;
public int enemiesPerWave = 5;

int spawnedCount = 0;
bool allSpawned = false;
bool wonAlready = false;

// inside SpawnEnemy(), after you Instantiate the enemy:
spawnedCount++;
if (spawnedCount >= totalWaves * enemiesPerWave)
{
allSpawned = true;
CancelInvoke(nameof(SpawnEnemy));
}

void Update()
{
if (allSpawned && !wonAlready
&& GameManager.instance != null
&& GameManager.instance.enemiesAlive <= 0)
{
wonAlready = true;
GameManager.instance.WinGame();
}
}
  1. Lines 1–2How many enemies total

    totalWaves * enemiesPerWave is the whole horde. Five waves of five slimes is 25 enemies. Both are public, so you can tune the length and density of the level.

  2. Lines 8–14Stop spawning when the horde is sent

    Each spawn bumps spawnedCount. Once it reaches the total, we flip allSpawned and CancelInvoke so the spawner stops. This is the same InvokeRepeating you set up earlier, now with an off switch.

  3. Lines 16–26Win only when empty

    Update waits until everything is spawned AND no enemies are left alive, then calls WinGame once. The wonAlready guard stops it from firing every frame after the win.

Build this GameObject

Win Panel (UI)

UI > Panel
Open recipe
Sprite
a 'Castle Defended' / 'You Win' label inside the panel
Active
UNCHECKED (inactive by default)
Wired into
GameManager → Win Panel field
Components to add
  • Panel (Image)
  • Text child

Same as the Lose Panel: full-screen, a victory message, start it UNCHECKED, and drag it into the GameManager's Win Panel slot.

Press Play. Defend through all five waves. Clear the last slime and the Win Panel appears. Let too many through and you lose instead.

Understand it

Notice where the decisions live. The gate is just a sprite. The enemy doesn't know what "losing" means — it only knows to call DamageGate and destroy itself. The spawner doesn't know what "winning" means — it only knows it ran out of enemies and the field is clear. The GameManager is the single place that turns those facts into an ending. Centralizing state like this is why you can reason about the game at all; if "are we losing?" were spread across ten scripts, you'd never find the bug.

Time.timeScale = 0 is the cheap, reliable way to freeze a game. Every Time.deltaTime-based motion and every InvokeRepeating timer respects it, so one line stops the whole simulation. The catch to remember for next stage: anything that unfreezes the game (like a restart) must set Time.timeScale back to 1, or your fresh game will start frozen.

Try this

Learning beat

Try this

Three short experiments. Predict before you run, then test your guess.

Predict first

Set gateHealth to 1. Before you play, decide: roughly how many slimes can slip past before you lose, and does that feel too punishing for wave 1? Run it, then pick a gateHealth that gives the player room to recover from a mistake.

Compare

Try totalWaves = 1 (a 5-enemy sprint) versus totalWaves = 5. Which length feels like a real match rather than a demo? Length is a design choice, and it's just a number.

Connect

Your GameManager now owns gold and gate health and the game-over flag. What else might belong there next stage — a sound to play on win, a score, a high score? Notice how the manager is becoming the game's single source of truth.

Test your stage

  • An enemy reaching the gate lowers the on-screen gate health.
  • Gate health at zero shows the Lose Panel and freezes the game.
  • Surviving all the waves with the gate intact shows the Win Panel.
  • The game ends only once — you never see both panels, and the win/lose can't fire twice.
  • Design check. Play one full match. Was the result fair — did you lose because of a real mistake, or win without breaking a sweat? Tune gateHealth, totalWaves, and enemy speed until a careless player loses and a thoughtful one wins.

If it breaks

  • The gate never takes damage. The enemy's gate check still has the old Debug.Log instead of DamageGate, or gateX doesn't line up with where slimes actually exit. Confirm the enemy calls GameManager.instance.DamageGate(damageToGate) and then Destroy(gameObject).
  • The panels never appear. They're probably still active in the Scene (so they show from the start) or not dragged into the GameManager's winPanel / losePanel slots. Uncheck the active box on each panel and wire both fields.
  • You win instantly. enemiesAlive likely never went above zero. Make sure Enemy.Start does enemiesAlive++ and OnDestroy does enemiesAlive--, and that the spawner has actually started spawning before Update checks for the win.
  • The game freezes and won't unfreeze. That's Time.timeScale = 0 doing its job. There's no restart yet — that's Stage 10. For now, press Play twice to reset.
  • Both panels show, or the ending fires repeatedly. The gameOver / wonAlready guards are missing or got removed. They're what make the ending happen exactly once.