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.
Published June 30, 2026
Defining the collision categories
Start by listing every distinct collision participant in the game. For a typical shmup, the minimum set is:
- Player body: The main player hitbox. Collides with enemy bullets and enemy bodies on contact.
- Player graze zone: Larger concentric zone around the player. Detects enemy bullet proximity only; does not trigger damage.
- Player bullets: All shots fired by the player. Collide with enemy bodies and certain environment objects.
- Enemy bodies: Physical presence of enemies. Collide with player bullets and player body (on kamikaze contact).
- Enemy bullets: All shots fired by enemies. Collide with player body and player graze zone only.
- Pickups: Items dropped by enemies. Collide with player body (collection trigger) and optionally with the world boundary. Must not collide with bullets.
- Environment: Stage geometry, walls, solid objects. Interacts selectively depending on game rules.
- Bomb field: The active area of a bomb/screen-clear weapon. Collides with enemy bodies and enemy bullets (to destroy them).
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 body | 1 | 5 (enemy bullets), 3 (enemy bodies), 6 (pickups) |
| Player graze zone | 2 | 5 (enemy bullets only) |
| Player bullets | 4 | 3 (enemy bodies), 7 (environment) |
| Enemy bodies | 3 | 4 (player bullets), 8 (bomb field) |
| Enemy bullets | 5 | 1 (player body), 2 (graze zone), 8 (bomb field) |
| Pickups | 6 | 1 (player body) — nothing else |
| Environment | 7 | (usually passive; only relevant if player/enemy movement is bounded) |
| Bomb field | 8 | 3 (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:
- 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.
- Flag check in handler: Keep the player body on its normal layer; add an
is_invincibleflag 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
- Player bullets hitting the player: Player bullet mask includes the player body layer. Fix by removing that bit from the mask.
- Pickups being destroyed by bullets: Pickup layer is included in player bullet mask. Fix by assigning pickups their own layer that no bullet mask references.
- Bomb not clearing bullets: Enemy bullet layer is not in the bomb field mask. Fix by adding the enemy bullet layer to the bomb mask.
- Graze zone triggering damage: The graze zone collision handler contains damage code instead of just graze-tick code. Architecture fix: graze zone emits a graze signal, never a damage signal.
- Enemy bullets passing through environment: Expected if environment is intentional background scenery. A bug if there are blocking walls — enemy bullet mask must include the environment layer if bullets should be blocked by it.
Documenting the intended matrix and periodically auditing the actual engine settings against it catches drift before it becomes a shipped bug.