W3D4: Live coding Duck Invaders (3/4)
Duck Invaders day three — sound design, audio mixing, an area-of-effect weapon, power-up drops and a screen shake.
Lucian Lazar · 26 Jul 2024
Third of four live coding sessions on Duck Invaders, and this is the one where the game starts to feel like a game rather than a physics demo. Lucian goes in without a plan again — he opens the GDD, decides what matters most with the deadline in mind, and works down the list. Sound first, because sound is the cheapest way to make something feel alive, then the second weapon type, then power-up drops. He’s blunt about the tradeoffs he’s making. Real duck models? The cost-to-reward isn’t there yet, so cubes and spheres stay until the last day. Sounds, though, are “visceral”, so those get done now.
What’s covered
Generating and wiring audio
- Running MusicGen in a Colab notebook to generate a two-minute synthwave soundtrack, including fighting the out-of-memory errors that come with the large stereo model (spoiler: medium mono is what survives)
- Pulling free SFX from Pixabay and keeping an attribution text file in the project, which he admits he’s lazy about but you shouldn’t be
- Trimming and normalising sounds in Audacity, and the unwritten rule he follows: SFX as WAV (raw, lower latency to start), music as MP3 or similar
- Setting up an AudioMixer with Master → Soundtrack and SFX groups, plus sends for ducking
Making sounds live in the right place
- Soundtrack as a looping 2D source with spatial blend at zero; hit and fire sounds as 3D so you hear them left/right
- Moving the fire sound off the projectile and onto its own throwaway game object that destroys itself after a second — not the most memory-efficient approach, and he says so, but it stops the sound being cut off when the bullet dies
- Hoisting
fireSoundinto the base projectile class so every projectile type gets it for free - A tiny
PitchRandomizercomponent with[RequireComponent(typeof(AudioSource))]so every duck dies at a slightly different pitch. This is the moment the game gets funny
Level flow and state
- A
FirstWavePreparedevent on the wave manager, a new warm-up state with a 1.5 second delay, and the level manager instantiating the soundtrack prefab off that event so the music kicks in just before the first wave - Stopping the soundtrack and playing a one-shot level-completed sound when waves finish
Enemies that fight back
EnemyEggSpawner— a coroutine that converts “eggs per minute” from the GDD into a spawn interval, attached to the blue, green and overgrown ducks with different values- Egg projectiles, the layer collision matrix, and confirming the player actually takes damage
- A health bar: scale X driven by normalised health, with the sprite colour lerped from red to green
Area-of-effect weapon
- Building the area bullet as a particle system plus a sphere collider, slower and weaker per-hit than the laser but hitting everything nearby
Physics.OverlapSpherewith a layer mask, collectingIDamageabletargets into a list of tuples paired with hit positions so the hit effect spawns in the right place for each victim- Adding a
Transformproperty to theIDamageableinterface, and why keeping it explicit on both implementers is fine
Power-ups
- An abstract
PowerUpbase with an abstractGive(Player), andWeaponSwitchPowerUpderiving from it PowerUpSpawner— a near-copy of the egg spawner, randomising interval between min and max and picking a random prefab from an array. Deliberately duplicated rather than shared, because they’ll change for different reasons- Prefab variants for the laser and AOE pickups, sharing a base with the particle trail and collider
- Giving the player an initial power-up list at start so the default weapon goes through the same code path as every drop
Timestamps
- 00:00:00 — Why there are now four live coding days instead of three, and the “unprepared on purpose” format
- 00:12:00 — Reading the GDD and deciding today’s priorities: sound, boss, eggs, area bullet, upgrades
- 00:16:00 — MusicGen in Colab, prompting for the soundtrack, and the first out-of-memory failure
- 00:22:00 — Soundtrack game object: AudioSource, loop, spatial blend at zero, play on awake or not
- 00:30:00 — Level completed sound from Pixabay, naming conventions, attribution file
- 00:40:00 — AudioMixer setup: master, soundtrack, SFX groups and the ducking send
- 00:48:00 — Audacity trim and export, WAV for SFX vs MP3 for music
- 00:56:00 — Laser fire sound, 3D spatial blend, and moving it onto its own self-destructing object
- 01:06:00 — Pushing
fireSoundinto the base projectile class so every projectile inherits it - 01:18:00 — Viewer question: has Lucian shipped commercial games (short answer: yes, mostly client work)
- 01:22:00 — Duck death sound and the
PitchRandomizercomponent - 01:35:00 —
FirstWavePreparedevent, warm-up state with delay, soundtrack instantiated from a prefab - 01:48:00 —
EnemyEggSpawnercoroutine, converting eggs-per-minute to intervals per duck type - 01:58:00 — Egg collisions, layer matrix, and adding the health bar with scale plus colour lerp
- 02:12:00 — Screen shake generated by ChatGPT, then debugged because it doesn’t offset from the original position
- 02:28:00 — Building the AOE projectile: particle system, sphere collider, sizing the radius against the visual
- 02:45:00 —
Physics.OverlapSphere, layer masks, and the tuple list of victims and hit points - 03:02:00 —
PowerUpabstract base,WeaponSwitchPowerUp, and who instantiates the weapon - 03:12:00 —
PowerUpSpawnerwith randomised intervals and random prefab selection - 03:30:00 — Git commit and push, then debugging zero damage in the debugger with F10/F11
- 03:40:00 — Balancing drop rates per duck, initial random wait before first spawn, playing a full level
What you build
By the end of this session the game has a generated soundtrack that fades in with the first wave, layered SFX routed through a mixer, ducks that quack at random pitches when they die, eggs raining down on the player, a visible health bar, a screen shake on overgrown duck deaths, a working area-of-effect weapon, and power-up pickups that drop from enemies and switch your weapon when you fly into them.
More usefully, you watch the debugging. The AOE weapon does no damage on the first playtest, and instead of guessing, Lucian attaches the debugger, steps into ReceiveDamage, sees the delta is zero, steps out twice and finds the base damage field was never set in the inspector. That whole loop takes about four minutes on screen.
[RequireComponent(typeof(AudioSource))]
public class PitchRandomizer : MonoBehaviour
{
[SerializeField] private float minPitch = 0.8f;
[SerializeField] private float maxPitch = 1.3f;
private void Awake()
{
GetComponent<AudioSource>().pitch =
UnityEngine.Random.Range(minPitch, maxPitch);
}
}
The collateral damage pass on the AOE projectile:
```csharp
var colliders = Physics.OverlapSphere(transform.position, areaOfEffectRadius, areaOfEffectLayer);
var victims = new List<(IDamageable, Vector3)> { (damageable, hitPosition) };
foreach (var collider in colliders)
{
if (collider.TryGetComponent(out IDamageable collaterallyDamageable))
victims.Add((collaterallyDamageable, collaterallyDamageable.Transform.position));
}
foreach (var (victim, hitPoint) in victims)
{
victim.ReceiveDamage(baseDamage);
if (hitEffect != null)
Instantiate(hitEffect, hitPoint, Quaternion.identity);
}
## Who should watch this
If you've done the earlier weeks and can write a MonoBehaviour but freeze when it's time to add audio, this is the session for you — it covers generating music, sourcing and editing SFX, mixing, and the small structural decisions that stop sounds getting cut off. It's also a decent look at how someone experienced uses an LLM for boilerplate like a screen shake and then immediately fixes the bugs in what it produced. Long, unedited, and occasionally messy, which is the point.
Part of the full curriculum. Course overview.