Stage 10: Polish and Ship
sound effects, a Skeleton enemy tier, a difficulty ramp, and a build
how audio, tuned numbers, and a second enemy turn a prototype into a game
a finished, balanced Castle Defense you can demo
The big idea
You already built a real game in Stage 9 — it can be won and lost. This stage is different from the others. There's no big new system to learn. Polish is a pile of small additions, each one tiny on its own, that together make the game feel finished: it makes noise when things happen, it has more than one kind of enemy, and it gets harder as you go.
That's the secret nobody tells you. The gap between "a prototype that works" and "a game people want to play" is mostly little touches. A hit that makes a sound feels twice as satisfying as a silent one — same code, plus four words. And almost everything here is data again: the Skeleton is your Enemy script with bigger numbers, and the difficulty ramp is just nudging those numbers up over time.
We add audio by giving the GameManager an AudioSource and a few AudioClips, then calling PlayOneShot at the right moments. We add a tougher Skeleton enemy with no new code. And we add a restart key so you can play again without leaving Play mode.
- AudioSource
- The component that actually plays sound. We put one on the GameManager so any script can ask it to play a clip.
- AudioClip
- One sound file (like enemy-hit.ogg) loaded into Unity. We hand clips to the AudioSource to play.
- PlayOneShot
- Plays a clip once, on top of whatever else is playing — perfect for stacking many quick effects like hits and coins.
- SceneManager
- Unity's tool for loading scenes. We use it to reload the current scene for an instant restart.
- Build
- Turning your project into a standalone app (an .exe or .app) that runs without the Unity Editor — what you hand to other people.
Finish Stage 9 — you need a working GameManager with win/lose, the WaveSpawner with finite waves, and the Enemy, TowerShooter, and Projectile scripts. You'll also need the six .ogg sounds from Setup imported into Assets/Audio.
Download what you need, then drag it into your Unity project's Assets folder. Keep the filenames exactly as shown so the steps match.
Art & audio from Kenney.nl — CC0 (public domain). Full credits live in CREDITS-unity-part1.md.
Build it
Step 1 — Give the GameManager a voice
We route all sound through the GameManager so any script can play an effect with one line. Add an AudioSource and the clips, plus a tiny PlaySound helper.
Make sure using UnityEngine.UI; is still at the top (from Stage 9). Add these fields to GameManager:
public AudioSource audioSource;
public AudioClip goldClip, hitClip, winClip, loseClip;
public void PlaySound(AudioClip c)
{
if (c && audioSource) audioSource.PlayOneShot(c);
}
Now call it from the moments that already exist — these are one-line edits, not new methods:
- In
AddGold, after you add the gold:PlaySound(goldClip); - In
WinGame, after the panel turns on:PlaySound(winClip); - In
LoseGame, after the panel turns on:PlaySound(loseClip);
Build this GameObjectAudioSource on the GameManager
Component on existing GameManager objectOpen recipe
AudioSource on the GameManager
Component on existing GameManager object- Play On Awake
- UNCHECKED (we trigger sounds ourselves)
- Audio Source (script field)
- drag this same AudioSource into the GameManager's Audio Source slot
- Gold Clip
- gold-pickup.ogg
- Hit Clip
- enemy-hit.ogg
- Win Clip
- win.ogg
- Lose Clip
- lose.ogg
- AudioSource
Add the AudioSource to the GameObject that already has GameManager.cs. Uncheck Play On Awake. Then drag each .ogg from Assets/Audio into the matching clip slot.
Press Play. You should now hear a coin sound when gold pays out, and a sting when you win or lose. Stop.
Step 2 — Make the towers and enemies noisy
The fire and hit sounds belong to the things that fire and get hit. These are tiny additions to scripts you already wrote.
In TowerShooter, right where it spawns a projectile, play the fire sound. (Use the archer's fire-arrow.ogg; the Cannon can point at cannon.ogg — see the note.)
if (GameManager.instance) GameManager.instance.PlaySound(/* fire clip */);
In Projectile.OnTriggerEnter2D, right where it damages an enemy, play the hit sound:
if (GameManager.instance) GameManager.instance.PlaySound(GameManager.instance.hitClip);
For the fire sounds, the cleanest approach is a public AudioClip fireClip; field on TowerShooter so the archer and the cannon can each carry their own sound (fire-arrow.ogg vs. cannon.ogg) — data, not code, exactly like Stage 8. Then call PlaySound(fireClip);.
Press Play and place a tower. Every shot and every hit now has a sound. The game suddenly feels alive — and you wrote almost nothing.
Step 3 — Add a Skeleton with no new code
Your second enemy is a Skeleton: tougher and faster than a slime. You know the move by now — it's your Enemy script with bigger numbers.
Build this GameObjectSkeleton prefab
2D SpriteOpen recipe
Skeleton prefab
2D Sprite- Sprite
- the skeleton sprite from your sliced tilemap
- Speed
- 2 (faster than a slime's ~1.5)
- Health
- 5 (a slime had ~3 — tougher)
- Damage To Gate
- 1
- Order in Layer
- 2
- Sprite Renderer
- Collider2D
- Enemy
Same Enemy.cs as the slime. Make it a prefab. NEW UNIT, NO NEW CODE — the Skeleton's whole personality is its Speed and Health numbers.
Now mix Skeletons into the waves. Your WaveSpawner has an enemyPrefab field; give it a second prefab and pick between them. The simplest version: add public GameObject skeletonPrefab; and, in SpawnEnemy, sometimes spawn the skeleton instead — for example Random.value < 0.3f spawns a Skeleton, otherwise a slime.
Press Play. Now and then a faster, tougher Skeleton appears. A single archer that handled slimes fine starts to struggle — which is exactly the pressure that makes the player build that Cannon from Stage 8.
Step 4 — A difficulty ramp and a restart key
A good game gets harder. The easiest ramp: send more enemies each wave. In WaveSpawner, raise enemiesPerWave (or enemy speed) a little as the wave number climbs — even enemiesPerWave += 1 per wave makes wave 5 feel like a real test after an easy wave 1.
Finally, add a restart so a player who loses (or wins) can go again instantly. This reloads the current scene. Add using UnityEngine.SceneManagement; to the top of GameManager, then put this in GameManager.Update():
The full script, line by line
The only real new code this stage: press R to unfreeze time and reload the current scene from scratch.
using UnityEngine;
using UnityEngine.SceneManagement;
// inside GameManager:
void Update()
{
if (Input.GetKeyDown(KeyCode.R))
{
Time.timeScale = 1f;
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}
Line 2SceneManagement namespace
SceneManager lives in UnityEngine.SceneManagement, so you must add this using line at the top or the script won't compile.
Line 7Detect the key once
Input.GetKeyDown(KeyCode.R) is true only on the single frame R is pressed, not while it's held — so one tap means one restart.
Line 9Unfreeze BEFORE reloading
Stage 9 set Time.timeScale to 0 on game over. We set it back to 1 first, or the reloaded scene would start frozen. This is the gotcha we flagged last stage.
Line 10Reload this exact scene
GetActiveScene().buildIndex is the scene you're in; LoadScene re-loads it fresh — full reset of gold, gate health, and waves. The cheapest, most reliable restart there is.
Press Play, win or lose, then press R. The game resets instantly and you're back to a full gate and zero waves.
Step 5 — Build the game (optional)
To hand your game to a friend, make a Build. Open File → Build Settings, click Add Open Scenes, pick your platform (Windows or Mac), click Build, and choose a folder. Unity produces a standalone app you can run without the Editor. That's a shipped game.
Understand it
Almost everything in this finale was data, not code — the lesson from Stage 8 paying off. The Skeleton is the slime's script with bigger numbers. The difficulty ramp is a number nudged up per wave. Even the per-tower fire sound is just a clip you drag into a field. The single genuinely new piece of code was the four-line restart, and it mattered mostly because it had to undo Time.timeScale = 0 first. When most of your "new features" are numbers and one-line calls into systems you already built, that's the sign your architecture is good.
Routing all audio through GameManager.PlaySound was a deliberate choice. We could have put an AudioSource on every tower and enemy, but then dozens of objects would each need a configured source and we'd have no single place to mute the game. One source, one helper, called from everywhere — the same "one brain" idea that made the gate logic clean in Stage 9.
Try this
Try this
Three short experiments. Predict before you run, then test your guess.
Set the Skeleton's Health to 10 and leave your towers alone. Before you play, decide: will a single archer ever kill one before it reaches the gate? Run it. If the answer is no, that's a difficulty knob — but make sure it's fun-hard, not impossible-hard.
Play one full match with all sounds on, then mute the game (or comment out the PlaySound calls) and play again. Same code, same balance — but notice how much flatter the silent version feels. Polish isn't decoration; it's information the player feels.
You finished a tower-defense game. The systems you built — spawning, placing units on a grid, projectiles, a resource economy, win/lose state, audio — show up in almost every game genre. Which one would you reuse first if you started a brand-new project tomorrow?
Test your stage
- Gold pickups, shots, hits, win, and lose all play their sounds.
- Skeletons appear in the waves and are clearly tougher/faster than slimes.
- The difficulty noticeably ramps up across waves.
- Pressing R restarts the game from a frozen win/lose screen, and the new game is not frozen.
- Design check. Play your whole game start to finish. Is it balanced? A new player should be able to lose the first time and win once they understand the trade-offs. Tune enemy numbers, costs, and the ramp until that's true — that final tuning pass is what separates a project from a game.
If it breaks during the demo
This is the finale, so here's a calm checklist for the moment someone watches you play.
- No sound at all. The
AudioSourceisn't dragged into the GameManager'saudioSourceslot, or Play On Awake is on (some setups misbehave). Confirm the source is assigned and each clip slot has its.ogg. - One sound plays but others don't. A clip slot is empty, or a
PlaySoundcall points at the wrong clip (a fire sound callinghitClip). Check each call names the right clip. - Skeletons act exactly like slimes. The prefab's
SpeedandHealthare still the slime's values, or the spawner never picks the skeleton prefab. Bump the numbers and confirmskeletonPrefabis assigned. - R does nothing. You probably forgot
using UnityEngine.SceneManagement;, or theUpdatewith the key check isn't actually on the GameManager object in the Scene. - The restart loads frozen. You're not setting
Time.timeScale = 1fbeforeLoadScene. Order matters: unfreeze, then reload. - The Build won't open the right scene. In Build Settings make sure your scene is in the list (Add Open Scenes) and checked.