W3D3: Live coding Duck Invaders (2/4)
Duck Invaders day two — starfield background, hand-placed enemy waves, an orbital spawner, a state-machine wave manager and hit effects.
Lucian Lazar · 25 Jul 2024
Second of four live coding sessions on Duck Invaders. Last time we got shooting, damage and death working. This one is about turning a pile of enemies into an actual level: three named waves, a wave manager that knows when each one is cleared, a title that fades in and out, and an “area cleared” message at the end. Along the way there’s a long detour into particle systems, because Lucian likes them and says so repeatedly.
What’s covered
Design document first, code second
- Filling in the blanks in the GDD: egg projectile damage (32), laser damage (15), chicken HP values for green, yellow and blue variants
- Lucian changing requirements on the fly and pointing out you normally don’t get that luxury — a real designer owns those numbers, and balancing them is an iterative job done in spreadsheets with testers
- Dropping the “starting health” field from the health component because it always equals max health. Don’t over-prepare for something you don’t know you need yet
Scrolling starfield background
- A rectangle-shaped particle system positioned above the camera frustum, rotated so particles fall down the screen
- Start size and start speed as random-between-two-constants, and a failed experiment with random-between-two-curves (the curve is time in the simulation, not a distribution — he works this out live)
- Two systems layered: many small slow stars, fewer big fast ones, with random-between-two-colours for a few blue and red ones
- Scaling mode and simulation space set so physics doesn’t touch them
Enemy variants and hand-built waves
- Green, yellow and blue ducks as prefab copies with different materials and health
- Wave 1 and 2 laid out by hand in the scene using grid snapping (Ctrl/Cmd + drag with snap set to 1 metre), roughly 50 ducks each
- An overgrown duck with 300 HP dropped into wave 3, demonstrating nested prefabs: change the source, the wave prefab updates
A scripted spawn wave
- Wave 3 spawns 50 blue ducks one at a time from a spawn point that orbits an origin, using cosine and sine
- The orbit radius grows each second so the ducks come out in a spiral rather than a ring
- A width-to-height ratio applied to the X axis so the orbit is an oval that fits the landscape screen
- Spawning is done with a coroutine and
WaitForSeconds, plus a warning that coroutines don’t resume if you disable and re-enable the object
Wave abstraction and the wave manager
- Abstract
Wavebase class with a title, a start delay and an abstractIsDone PredefinedWavecollects its ducks withGetComponentsInChildrenin Start and reports done when they’re all null- The scripted wave reports done when all units have spawned and all spawned units are dead
- A short explanation of Unity’s overloaded null operator — a destroyed object compares equal to null even though C# says otherwise
- The wave manager refactored into an enum-based finite state machine:
Init,WaitForWaveStartDelay,WaveRunning,AllWavesFinished - Cognitive complexity: early returns, flatter code, explicit names. His argument is it pays off a month later, not today
UI, tweening and a live bug hunt
- TextMeshPro wave title on a canvas, anchored to the top, scale-with-screen-size
CrossFadeAlphaandCrossFadeColorboth refuse to work, so DOTween gets imported andDOFadedoes the job in one line- Assembly definitions: referencing TMP and DOTween from the game assembly, and adding an asmdef to the Plugins folder so DOTween’s setup menu appears
- Attaching the debugger to Unity to find why waves were skipping —
PrepareNextWaveandSpawnWavewere both incrementing the index
Feedback effects
- Per-duck death particle prefabs, scaled up for the overgrown duck
- A tiny
DestroyDelaycomponent that removes its own game object after N seconds - Projectile hit effect spawned at
collision.contacts[0].point, which meant passing theCollisionthroughApplyDamage
Timestamps
- 00:00:40 — Audio check and the plan: wave system, wave transitions, level complete text
- 00:04:30 — Back to the GDD: egg projectile damage, chicken HP, laser damage, and why balancing is the designer’s job
- 00:12:00 — Green chicken prefab, material, and dropping the redundant starting-health field
- 00:18:30 — MusicGen as a free local tool for generating a boss soundtrack
- 00:23:00 — Starfield particle system: shape, rotation, positioning above the camera
- 00:40:00 — Random between two curves versus two constants, and why the curve wasn’t what he wanted
- 00:56:00 — Second particle layer, colour randomisation, tuning emission and speed
- 01:08:00 — Yellow and blue duck prefabs via copy-paste and material swaps
- 01:16:00 — Wave 1 laid out by hand with grid snapping; counting to 50 the hard way
- 01:30:00 — Wave 2, and deciding wave 3 should spawn from a script instead
- 01:36:00 — Wave 3 script fields: unit prefab, spawn interval, max units, orbit centre and angular speed
- 01:48:00 — Cosine/sine orbital math, degrees to radians, and asking ChatGPT to fill in the boilerplate
- 02:00:00 — Oval shape via width-to-height ratio, and growing the radius for a spiral spawn
- 02:10:00 — Testing wave 3, fixing the stray first-frame spawn by moving the spawn point once in Start
- 02:16:00 — Overgrown duck and nested prefab propagation
- 02:24:00 — Abstract
Wavebase class,PredefinedWave, and Unity’s null trick for destroyed objects - 02:42:00 — Wave manager as an enum state machine, plus the cognitive complexity rant
- 03:00:00 — Canvas, TextMeshPro title, and referencing TMP from an assembly definition
- 03:10:00 — CrossFadeAlpha fails, DOTween gets imported,
DOFadein one line - 03:22:00 — Debugger attached: finding the double index increment
- 03:30:00 — LevelManager subscribing to the wave manager’s finished event, “area cleared”
- 03:38:00 — Death particle prefabs per duck type and the
DestroyDelaycomponent - 03:46:00 — Projectile hit effect spawned at the contact point
What you build
By the end you have a playable level one: three named waves with start delays and fade-in titles, two of them hand-placed and one spawned in a spiral by script, an overgrown duck sitting in the middle of the last wave, death particles on every enemy, hit sparks at the exact contact point, a scrolling star background, and a level manager that shows “area cleared” when the last duck dies. The boss and egg-laying behaviour are deliberately pushed to a later session — the point was to get a whole game loop standing before polishing any single piece.
Code
The orbital spawn point, roughly as it ended up:
private void MoveSpawnPoint()
{
_currentOrbitAngle += spawnPointOrbitAngularSpeed * Time.deltaTime;
_currentSpawnPointDistance = Mathf.Min(
_currentSpawnPointDistance + spawnPointDistanceIncreasePerSecond * Time.deltaTime,
maxSpawnPointDistance);
var radians = _currentOrbitAngle * Mathf.Deg2Rad;
var offset = new Vector3(Mathf.Cos(radians) * widthToHeightRatio, Mathf.Sin(radians), 0f);
spawnPoint.position = spawnPointOrbitCenter.position + offset * _currentSpawnPointDistance;
}
And the state machine that drives the waves:
```csharp
private enum EState { Init, WaitForWaveStartDelay, WaveRunning, AllWavesFinished }
private void Update()
{
switch (_state)
{
case EState.Init:
PrepareNextWave();
break;
case EState.WaitForWaveStartDelay:
_startDelayRemaining -= Time.deltaTime;
if (_startDelayRemaining <= 0f) SpawnWave();
break;
case EState.WaveRunning:
if (!_currentWave.IsDone) return;
DestroyWave();
if (_currentWaveIndex >= wavePrefabs.Length - 1)
{
_state = EState.AllWavesFinished;
AllWavesFinished?.Invoke();
return;
}
PrepareNextWave();
break;
}
}
## Who should watch this
This is for anyone who can already move a player and shoot things but has no idea how to structure a level. The wave abstraction, the base class plus two implementations, and the enum state machine are the transferable parts — they'd work just as well in a tower defence or an arena shooter. Sit through the particle tuning too, since you'll see the actual trial-and-error rather than a finished settings screenshot.
Part of the full curriculum. Course overview.