Game Programming · August 9, 2026
Debugging Collision Detection Bugs in 2D Shmups
Collision bugs undermine a shmup more than almost any other category of defect. A bullet that passes through an enemy is merely annoying. A player death with no visible bullet nearby makes the game feel broken and unfair, and players will not distinguish between a genuine bug and a design choice that looks like a bug. Debugging collision issues requires a specific set of tools and techniques that differ meaningfully from debugging general software.
Published August 9, 2026
The first rule of debugging collision issues: never trust your eyes alone. Sprites are drawn at their visual positions, which may not match the collision bounds your code is actually testing. The gap between what a player sees and what the engine tests is where most collision bugs hide. Every collision debugging session should start by enabling a visual overlay that draws your actual hitboxes on screen, separate from the sprites.
Building a hitbox debug overlay
A debug overlay draws the bounding shapes your collision code uses directly on screen, typically as colored outlines or semi-transparent fills. For AABB (axis-aligned bounding box) collision, that means rectangles. For circle collision, circles. The overlay should be togglable with a key press so you can enable it mid-game without restarting.
Enabling this overlay almost always produces an immediate revelation: hitboxes that are offset from sprites due to incorrect origin assumptions, bounding boxes that are far larger than the visible sprite, or player hitboxes that were accidentally set to a radius of zero (making the player immune to all damage). Every single one of these bugs looks correct in the game without the overlay and wrong the instant you enable it.
Tunneling: when fast bullets skip through targets
Tunneling occurs when a fast-moving object travels far enough in a single simulation step that it crosses entirely through a target without the collision check registering a hit. At 60 FPS, a bullet moving at 800 pixels per second advances 13 pixels per tick. An enemy with an 8-pixel hit radius can be entirely skipped if the bullet's center was 5 pixels before the near edge of the enemy on one tick and 5 pixels past the far edge on the next.
The symptoms of tunneling are inconsistent: bullets sometimes pass through enemies they should hit, and the failure rate increases with bullet speed and decreases with enemy size. It is more common in custom engines than in framework-provided physics, which often include sweep-based collision detection to handle exactly this case.
The standard fix for moderate speed bullets is continuous collision detection (CCD): instead of testing the bullet's current position against all targets, trace a line segment from the bullet's previous position to its current position and test that segment for intersection.
For shmup bullets, which are typically small and travel primarily in one direction, a simpler approximation is often sufficient: subdivide the bullet's movement into steps no larger than the smallest target's radius, and test each sub-step. This is less mathematically precise than true swept collision but catches the vast majority of tunneling cases at lower implementation cost.
The off-by-one-frame ghost death
One of the most common shmup collision bugs is the ghost death: the player dies on a frame where no bullet appears to be at their position. The cause is almost always a frame ordering issue. Enemy bullets are updated (positions advanced), then collision is checked, then the player and bullets are drawn. If a bullet passed through the player's position during the update step but was already past it by the time the collision check ran, the visual shows the bullet past the player but the collision logic sees a hit.
The correct fix is to check collision immediately after movement, before any state changes take effect. Specifically, after advancing each enemy bullet's position, check it against the player before advancing the next bullet. Do not batch all movements and then check all collisions; the interleaving matters.
Broadphase and performance at high bullet counts
Naive collision detection tests every bullet against every potential target. With 500 enemy bullets and 100 player bullets on screen, that is 50,000 checks per tick for enemy-to-player and 50,000 checks for player-bullet-to-enemy, plus enemy-to-player-bullet checks. At 60 FPS, that is 6 million checks per second. Modern hardware handles this without issue for these counts, but it is worth knowing where the limit is.
If you encounter performance problems from collision checks — visible in a profiler as excessive time in the collision pass — the standard solution is broadphase partitioning: divide the screen into a grid of cells, assign each object to the cell containing its center, and only test collision between objects that share a cell. This reduces average check count dramatically for large bullet counts with spatially local collision (which is always the case in a shmup).
Replay debugging for intermittent bugs
Some collision bugs are intermittent: they appear only under specific game states, timing combinations, or bullet configurations that are hard to reproduce intentionally. A deterministic replay system — one that records all input events and uses a fixed-timestep simulation to produce exactly the same sequence of game states from those inputs — is the most powerful tool for this class of bug. Record a play session where the bug occurred, replay it with the debug overlay enabled, and step through the frames around the failure at reduced speed until the collision discrepancy is visible.
Even without a full replay system, adding a frame-by-frame advance mode (pause the game, then step one tick at a time with a key press) combined with the hitbox overlay makes previously invisible collision events legible. Building this tool early in development pays off repeatedly throughout the project.