Skip to main content

Stage 2: Waves

Course progressStage 2 of 10
~45 min
Build

a spawner that drops slimes onto random lanes on a timer

Learn

prefabs and Instantiate, and how an array of lane points makes lanes

Ship

an endless trickle of slimes you can speed up or slow down

The big idea

In Stage 1 you placed one slime by hand and gave it a script. That's fine for one enemy. A tower-defense game needs dozens, and you can't drag each one into the Scene yourself.

The trick is a prefab. A prefab is a saved copy of a GameObject — sprite, components, Inspector values, all of it — stored as a file in your Project. Once the slime is a prefab, your code can stamp out fresh copies of it at runtime with one line: Instantiate. Each copy already has the Enemy script from Stage 1, so each new slime walks left on its own.

We also want slimes to arrive on different lanes — different rows of the courtyard. To do that, we keep a list of spawn points. In code, a list of things of the same type is called an array. We'll fill an array with a few empty marker objects (one per lane), then pick one at random each time we spawn. Picking randomly is what makes a wave feel alive instead of a single-file line.

New words
Prefab
A saved GameObject template stored in your Project. Your code can create copies of it while the game runs.
Instantiate
The Unity method that creates a new copy of a prefab in the Scene at a position you choose.
Array
A numbered list of things of the same type. lanes[0] is the first, lanes[1] the second, and lanes.Length is how many there are.
InvokeRepeating
A Unity method that calls one of your methods over and over on a timer.
Random.Range
Gives back a random number in a range. With two whole numbers it stops one short of the high end — perfect for picking an array index.
Before you start

Finish Stage 1: The Courtyard and the First Enemy — you need a Slime GameObject that walks left because it has the Enemy script attached.

Build it

Step 1 — Turn the slime into a prefab

Drag your Slime GameObject from the Hierarchy down into a folder in the Project panel (make an Assets/Prefabs folder first). The Slime's name turns blue in the Hierarchy — that means it's now linked to a prefab file. You now have a reusable template.

Build this GameObject

Slime (Prefab)

Prefab asset
Open recipe
Sprite
the green slime from your sliced tilemap
Made from
the Stage 1 Slime, dragged into Assets/Prefabs
Components to add
  • Sprite Renderer
  • Enemy.cs

Once it's a prefab, delete the Slime that's still sitting in the Scene. From now on the spawner creates slimes; you don't place them by hand.

Delete the loose Slime from the Hierarchy. The Scene should now have no slimes in it — the spawner will make them.

Step 2 — Add lane markers and a spawner

A lane is just a position. We mark each one with an empty GameObject at the right edge of the courtyard, at a different Y. Then one more empty object holds the spawner script.

Build this GameObject

Lane

Empty GameObject (×3)
Open recipe
Lane 1 Position
(8, 2, 0)
Lane 2 Position
(8, 0, 0)
Lane 3 Position
(8, -2, 0)

Make three: right-click in the Hierarchy → Create Empty. Name them Lane1, Lane2, Lane3. Same X (the right edge), different Y so the rows are stacked.

Build this GameObject

Wave Spawner

Empty GameObject
Open recipe
Position
(0, 0, 0)
Components to add
  • WaveSpawner.cs (added in Step 3)

The spawner has no sprite — it's an invisible manager object. Its only job is to run the WaveSpawner script.

Step 3 — Write the spawner

Select the Wave Spawner object, click Add Component → New Script, name it WaveSpawner, and build it in three passes. Press Play after each one.

Pass 1 — wire up the prefab and the lanes. First we just prove the spawner can see its prefab and its array of lanes.

using UnityEngine;

public class WaveSpawner : MonoBehaviour
{
public GameObject enemyPrefab;
public Transform[] lanes;

void Start()
{
Debug.Log(lanes.Length);
}
}

Save and return to Unity. Select the Wave Spawner. In the Inspector you'll see two empty fields: Enemy Prefab and Lanes. Drag your Slime prefab from the Project panel into Enemy Prefab. For Lanes, set its Size to 3, then drag Lane1, Lane2, and Lane3 from the Hierarchy into the three slots. Press Play. The Console prints 3 — your array is wired correctly. Stop.

Pass 2 — spawn one slime. Now we actually create a slime at the first lane. Replace the Start method and add a SpawnEnemy method:

void Start()
{
SpawnEnemy();
}

void SpawnEnemy()
{
Instantiate(enemyPrefab, lanes[0].position, Quaternion.identity);
}

Press Play. One slime appears at Lane1's position and walks left — because the copy carries the Enemy script from Stage 1. You wrote no movement code here; the prefab already knows how to walk. Stop.

Pass 3 — random lanes on a timer. A single slime isn't a wave. We pick a random lane and repeat on a timer:

Script anatomy

The full script, line by line

The finished Stage 2 spawner: every couple of seconds it stamps a fresh slime onto a random lane.

using UnityEngine;

public class WaveSpawner : MonoBehaviour
{
public GameObject enemyPrefab;
public Transform[] lanes;
public float spawnInterval = 2f;

void Start()
{
InvokeRepeating(nameof(SpawnEnemy), 1f, spawnInterval);
}

void SpawnEnemy()
{
Transform lane = lanes[Random.Range(0, lanes.Length)];
Instantiate(enemyPrefab, lane.position, Quaternion.identity);
}
}
  1. Lines 5–7The data the spawner needs

    enemyPrefab is the template to copy. lanes is the array of spawn points you filled in the Inspector. spawnInterval is the gap between spawns, in seconds — public, so you can tune it during Play.

  2. Line 11InvokeRepeating runs a method on a timer

    nameof(SpawnEnemy) names the method to call. The 1f waits one second before the first spawn; spawnInterval is the gap between every spawn after that.

  3. Line 16Random.Range picks a lane

    Random.Range(0, lanes.Length) returns a whole number from 0 up to but not including lanes.Length. With 3 lanes that's 0, 1, or 2 — a valid index every time, even if you add more lanes later.

  4. Line 17Instantiate stamps a copy

    It creates a new slime from the prefab at the chosen lane's position. Quaternion.identity just means 'no rotation.' Each copy starts walking on its own.

Press Play and watch. Every two seconds a new slime appears on a random lane and crawls toward the gate. While it plays, lower Spawn Interval in the Inspector and the wave thickens; raise it and the slimes thin out.

Understand it

Instantiate plus a prefab is the engine of almost every game that creates things while it runs — bullets, coins, enemies, particles. The reason it works so cleanly is that the prefab already contains the slime's whole setup. The spawner doesn't know or care how a slime walks; it just makes one and trusts the Enemy script to do the rest. That separation — one object spawns, another object behaves — is what lets the game grow without any single script getting huge.

We chose InvokeRepeating over checking a timer by hand in Update() because it's less code and reads clearly: "call this method every spawnInterval seconds." The trade-off is that it's less flexible — you can't easily change the rhythm mid-wave. Later stages that ramp difficulty will move to a manual timer for exactly that reason, but for a steady trickle, InvokeRepeating is the right tool.

Try this

Learning beat

Try this

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

Predict first

Set Spawn Interval to 0.5 before you press Play. Decide first: roughly how many slimes will be on screen at once compared to 2? Run it and see if the courtyard gets crowded the way you expected.

Compare

Temporarily change lanes[Random.Range(0, lanes.Length)] back to lanes[0] and run it. Every slime stacks on one lane — a single-file line. Switch back to the random version. Which feels more like a real wave, and why does randomness matter for a defense game?

Connect

Right now every slime is identical. If you wanted a rare fast slime to mix in, you already have the tool from Stage 1 (a public field on the prefab) and a tool from this stage (Random.Range). How might you combine them?

Test your stage

  • The loose Slime is gone from the Hierarchy; only Lane markers and the Wave Spawner remain before you press Play.
  • In Pass 1, the Console printed 3 (your lanes array is wired).
  • Pressing Play spawns slimes that appear on different lanes and walk left on their own.
  • Changing Spawn Interval during Play makes the wave thicker or thinner.
  • Design check. Let it run for thirty seconds. Does the trickle feel like a steady, beatable pressure, or does it flood the screen? Pick a spawn interval and lane spacing that a player could realistically defend against.

If it breaks

  • The Console says "NullReferenceException" or the spawner does nothing. You probably didn't drag the Slime prefab into the Enemy Prefab slot, or the Lanes array is still empty. Select the Wave Spawner and check both fields in the Inspector.
  • Only one slime ever appears. You may still be on Pass 2 (SpawnEnemy() called once in Start) instead of Pass 3's InvokeRepeating. Confirm the Start method calls InvokeRepeating.
  • An error mentions "index out of range." The Lanes array Size is smaller than the lanes you tried to use, or you hardcoded lanes[3] when there are only three lanes (which are 0, 1, 2). Trust Random.Range(0, lanes.Length) to stay in bounds.
  • Slimes spawn but don't move. The slimes are fine but the prefab lost its Enemy script. Open the prefab (double-click it in the Project) and confirm Enemy (Script) is on it.
  • Slimes spawn in a pile at the center. Your Lane markers are all at (0, 0, 0). Give each a different position out at the right edge.