Week 3 — Build a Full Game: Duck Invaders · w3d3 · 3h 52m

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 Wave base class with a title, a start delay and an abstract IsDone
  • PredefinedWave collects its ducks with GetComponentsInChildren in 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
  • CrossFadeAlpha and CrossFadeColor both refuse to work, so DOTween gets imported and DOFade does 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 — PrepareNextWave and SpawnWave were both incrementing the index

Feedback effects

  • Per-duck death particle prefabs, scaled up for the overgrown duck
  • A tiny DestroyDelay component that removes its own game object after N seconds
  • Projectile hit effect spawned at collision.contacts[0].point, which meant passing the Collision through ApplyDamage

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 Wave base 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, DOFade in 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 DestroyDelay component
  • 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.