Quick answer: Wave spawning bugs are state and timing bugs. Capture which wave was active, its progress, which spawn points fired, the spawn timing, and the count of living enemies with every report. With that snapshot you reproduce enemies appearing inside geometry, waves that never complete, or spawn floods, instead of replaying the whole run hoping it recurs.

Wave spawning sits at the heart of horde shooters, survivors-likes, and tower defense, and it fails in ways that feel random to players. An enemy spawns inside a wall. A wave never ends because the last enemy spawned out of bounds and the counter never decrements. A late wave dumps twice as many enemies as intended and the framerate collapses. These bugs depend on the exact state of the wave system and the timing of spawns, which is fragile and hard to recreate by hand. To debug them you need a snapshot of the spawner's state at the moment things went wrong. This post covers what to capture and why timing makes it so slippery.

Wave bugs are state machine bugs

A wave system is a state machine: idle, spawning, in-progress, cleared, transitioning to the next wave. Most wave bugs are really transitions that fired wrong. The classic never-ending wave happens when the cleared transition depends on the living-enemy count reaching zero, and one enemy spawned somewhere it can never die, so the count never reaches zero and the wave hangs. Understanding the bug means knowing which state the system was in and what triggered or failed to trigger the transition. A report that just says the wave never ended omits the only information that matters.

Because the state advances over time and in response to events, the same inputs in a different order produce different outcomes. A player who kills enemies quickly hits a different code path than one who lets them pile up. Capturing the wave index, the spawn progress within the wave, the living-enemy count, and the pending-spawn queue tells you exactly where the machine was when it broke. With that you can set up the same state in a test scene and watch the transition fail, rather than playing through many waves hoping to recreate the conditions.

Spawn points and enemies in geometry

Enemies spawning inside walls or under the floor are placement bugs, and they hinge on which spawn point fired and what was around it. Capture the spawn point identifier, its world position, and the enemy type spawned. Often the spawn point itself is fine and the problem is a check that was skipped: no overlap test against geometry, no navmesh validity test, or a spawn that happened during a moment when a door or platform was mid-animation and occupying the spawn volume. The state of nearby dynamic geometry at spawn time is part of the bug.

Some spawn placement bugs are conditional on player position. A spawner that picks the point furthest from the player can choose a point outside the playable area if the player stands in an unexpected spot, like on top of a prop. Recording the player position alongside the spawn point reveals these relationships. The pattern repeats across wave systems: the spawn that went wrong is a function of the spawner state, the chosen point, and the surrounding world at that instant, and you need all three to reproduce an enemy materializing inside a rock.

Timing is the slippery part

Wave systems are timing heavy. Spawns are scheduled on timers, waves transition after delays, and difficulty scaling reads the elapsed time. Timing bugs are the hardest to reproduce because they depend on frame rate, on whether the player paused, on hitches that delayed a tick, and on the order timers resolved within a frame. A wave that double-spawns might do so only when a frame ran long enough that two scheduled spawns landed in one update. Capturing timestamps for recent spawns and the wave's internal timers gives you the timeline you need to spot a coincidence like that.

Frame-rate dependence is a frequent culprit. If spawn intervals are computed by subtracting delta time without accumulating remainder, then at low frame rates you spawn fewer enemies and at high frame rates you spawn more, so the same wave behaves differently per machine. Capture the player's frame rate or recent frame times along with the timing state. When you can see that a report came from a machine running at thirty frames per second and the double-spawn correlates with a long frame, you have your lead, and you can reproduce it by throttling your own update loop.

Make spawning deterministic enough to test

You cannot make a live action game fully deterministic, but you can make the spawn logic itself deterministic given its inputs. Drive spawn randomness from a seeded stream so a given wave picks the same points and enemy types every time. Decouple the spawn schedule from frame rate by accumulating time and spawning whatever the accumulated budget allows, so behavior is consistent across machines. With these in place, the wave state you captured becomes a reproducible scenario: load wave seven at the captured timing and seed, and the same enemies spawn at the same points.

Build a test harness that lets you start any wave directly, with a chosen seed and an optional fixed time step. This turns wave debugging from a marathon into a unit test. You can fast-forward to the wave a player reported, freeze the clock, and step spawn by spawn to find the one that lands in a wall or the one that escapes the kill counter. A deterministic spawner plus a wave-jump command is the most powerful debugging combination this genre offers, and it pays back the setup cost on the first nasty report.

Setting it up with Bugnet

Bugnet's in-game report button captures device and platform context automatically, including the kind of frame-rate information that wave timing bugs hinge on. You add custom fields for the active wave index, spawn progress, living-enemy count, recent spawn timestamps and points, and the spawn seed. Because the button snapshots state when the player reports a never-ending wave, you receive the exact stuck count and the spawn that escaped the counter, not a guess. That snapshot is the seed of a reproduction you can run on your own build in minutes.

Occurrence grouping is valuable because spawn bugs cluster around specific waves or spawn points. If wave twelve consistently spawns an enemy out of bounds, you will get many reports that Bugnet folds into one issue with a count, making the pattern obvious. Filter by wave index using your custom field to isolate the offending wave, filter by frame rate to confirm a timing dependence, and keep every spawn snapshot in one dashboard. Instead of a vague trickle of wave hung complaints you get a precise, prioritized list of the spawners and transitions that actually break.

Test the edges and ship with confidence

Spawn bugs hide at the edges: the first wave, the last wave, the boss wave, the moment difficulty scaling crosses a threshold, and the transition when the player clears a wave faster than the spawner expected. Write tests that drive the spawner through these boundaries with fixed seeds and time steps, asserting that enemy counts match intent and that every spawned enemy lands on valid ground. Each fixed bug should leave behind a test that recreates the captured wave state, so a future tweak to spawn timing cannot silently reintroduce the never-ending wave you already fixed.

Treat the spawner as a system you can pause, inspect, and replay rather than a black box that occasionally misbehaves. Once you capture wave state with reports, make spawning deterministic, and add wave-jump tooling, the genre's signature bugs become tractable. Players stop hitting stuck waves because you caught the escaped-enemy case in a test, and the ones that do slip through arrive as reproducible snapshots. That is how you keep a wave-based game feeling fair and stable as you add ever more elaborate waves on top of it.

Wave bugs are state machine and timing bugs. Snapshot the wave state and spawn timing and the never-ending wave finally becomes reproducible.