Game Programming · June 30, 2026
Entity-Component Systems in Shmups: Composition for a World Full of Moving Parts
A vertical shmup with five stages might have thirty distinct entity types: six enemy ship variants, three boss forms, four bullet types, two powerup kinds, debris, explosions, a player ship, shields, and various background elements. An inheritance-based class hierarchy for all of these will eventually produce either an enormous base class that every entity inherits, or a diamond inheritance nightmare where a homing bullet needs behaviors from both the Bullet class and the Seeker class. Entity-component systems replace the hierarchy with composition, and the result is entity variety that scales without cascading refactors.
Published June 30, 2026
The ECS model has three parts. An entity is an identifier — nothing more than an integer or a UUID. A component is a plain data struct attached to an entity, carrying state but no behavior. A system is a function that queries for all entities holding a specific set of components and operates on those components. The player ship is not a PlayerShip class; it is entity 42, which happens to have a Position component, a Velocity component, a Sprite component, a Health component, an InputHandler component, and a WeaponMount component. A bullet is entity 103 with Position, Velocity, Sprite, Lifetime, and Damage components — no Health, no InputHandler. A powerup is entity 201 with Position, Sprite, Lifetime, and PickupEffect components.
Why inheritance hierarchies break down in shmups
The trouble begins when two entity types share a behavior that does not fit neatly into a shared parent. A shield pickup and a bomb powerup both need to be picked up when the player touches them, both have a visual appearance, and both disappear after a timeout. But a shield also has a health value and can be destroyed by enemy fire before it is picked up — a behavior it shares with a destructible obstacle, not with a powerup. Modeling this correctly in an inheritance hierarchy forces a choice between a very wide base class, multiple inheritance, or awkward mixin composition that most OOP languages handle poorly.
In ECS, the destructible shield is just an entity with both a PickupEffect component (like other powerups) and a Health component (like enemies). The collision system that handles bullet-vs-destructible interactions queries for entities with Health and Collidable; the pickup system queries for entities with PickupEffect and Collidable. The shield entity participates in both queries. No inheritance required.
Core shmup components
The minimal component set for a working shmup:
| Component | Fields | Used by |
|---|---|---|
| Position | x, y | All visible entities |
| Velocity | dx, dy | Bullets, enemies, player |
| Sprite | texture_id, frame, layer | All visible entities |
| Collider | radius, layer_mask | Player, enemies, bullets, pickups |
| Health | current, max | Player, enemies, destructibles |
| Damage | amount, on_hit_effect | Bullets, hazards |
| Lifetime | remaining_frames | Bullets, explosions, pickups |
| InputHandler | action_map | Player only |
| Emitter | pattern_id, fire_timer | Enemies, some bosses |
| PickupEffect | effect_type, value | Powerups |
System execution order
Systems must run in a defined order each frame because they modify the same shared data. The standard shmup update order:
Cleanup must run last, after all systems have finished reading the entity list for the current frame. Destroying an entity mid-frame while other systems are still iterating over it is the most common source of crash bugs in ECS implementations. Queue destruction events and process them once per frame at the end.
Performance characteristics
A canonical ECS with components stored in contiguous arrays per type (struct-of-arrays layout) has excellent cache performance for systems that operate on a single component type at a time. The movement system iterates over all Position and Velocity arrays sequentially — two cache-friendly linear passes. The collision system is harder because it needs Position and Collider together, but with sorted entity lists and a spatial hash the broad phase remains manageable even at 800+ simultaneous entities.
In scripting-language environments (GDScript, Lua), the ECS model can underperform a naive class-based approach because the overhead of component lookup via dictionary or array indexing exceeds the cache benefit at small entity counts. Profile before committing: for games with under 200 simultaneous entities, a well-structured class hierarchy with pooled instances often performs similarly to ECS and is simpler to debug.
When not to use ECS
ECS is a tool for a specific problem: managing a large number of entities with varying combinations of shared behavior. If the shmup has five enemy types that all behave similarly and differ only in art and stats, a simple subclass per type is easier to read and maintain. ECS becomes valuable when the entity variety is high, when behaviors need to be added and removed at runtime (a ship that gains a shield component when it picks up a power-up), or when the entity count is high enough that data layout matters for performance. Neither of those conditions holds for every project, and adopting ECS architecture without the problem it solves produces accidental complexity without the corresponding benefit.