Flukz — open-source shmup community resource open-source GPL devlog  ·  about
// indie shmup & game-dev resource

Game Programming · June 30, 2026

Collision Layer Architecture in Shmups: Masks, Zones, and Isolation

A shmup's collision system handles a higher volume and variety of interactions than almost any other genre. Player bullets must hit enemies but not the player. Enemy bullets must hit the player but not enemies. Pickups must be collectible by the player but should not block or be destroyed by bullets. Graze zones must detect bullet proximity without triggering damage. Environment geometry may need to block movement but not hitbox detection. Getting this wrong does not just produce bugs — it produces the kind of subtle bugs that only appear at runtime under specific conditions and are difficult to reproduce.

Defining the collision categories

Start by listing every distinct collision participant in the game. For a typical shmup, the minimum set is:

The layer-mask interaction matrix

The interaction matrix defines which layer each entity is on (its layer) and which layers it looks for (its mask). A collision between two objects only occurs if object A's mask includes object B's layer, or vice versa depending on engine conventions.

Entity Layer Mask (detects)
Player body15 (enemy bullets), 3 (enemy bodies), 6 (pickups)
Player graze zone25 (enemy bullets only)
Player bullets43 (enemy bodies), 7 (environment)
Enemy bodies34 (player bullets), 8 (bomb field)
Enemy bullets51 (player body), 2 (graze zone), 8 (bomb field)
Pickups61 (player body) — nothing else
Environment7(usually passive; only relevant if player/enemy movement is bounded)
Bomb field83 (enemy bodies), 5 (enemy bullets)

This matrix is the single most important document to maintain in your project's technical documentation. When a collision bug appears, the first step is checking whether the matrix has drifted from the actual engine configuration — accidental bit flag changes in prefabs and scenes are the most common cause of mysterious late-stage collision regressions.

Separating detection from damage

A common mistake is using the physics collision signal to directly deal damage in the handler function. This creates tight coupling between the collision system and the damage system and makes it impossible to add events like invincibility frames, shield absorption, or block effects without modifying both systems.

A cleaner architecture separates the two: the collision signal fires a hit_detected(source, type) event, and the damage system subscribes to that event and applies damage after consulting the current state (invincible, shielded, blocking). The collision layer system never knows about health values or game states.

Invincibility frame isolation

After taking damage, players typically receive a brief invincibility window. Two implementation approaches:

  1. Layer swap: During invincibility, temporarily move the player body to a layer that no enemy bullet mask includes. This prevents any physical collision detection from firing. It is simple but requires restoring the layer accurately on expiry — if the restoration fails, the player is permanently invincible.
  2. Flag check in handler: Keep the player body on its normal layer; add an is_invincible flag that the hit handler checks before applying damage. The collision still fires during invincibility, but the damage application is blocked by the flag.

The flag-check approach is safer and easier to debug. The layer-swap approach is marginally more performant (fewer false positive collision signals) but the performance difference is not meaningful at normal shmup bullet counts.

Common mistakes and how they manifest

Documenting the intended matrix and periodically auditing the actual engine settings against it catches drift before it becomes a shipped bug.