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

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.

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
Positionx, yAll visible entities
Velocitydx, dyBullets, enemies, player
Spritetexture_id, frame, layerAll visible entities
Colliderradius, layer_maskPlayer, enemies, bullets, pickups
Healthcurrent, maxPlayer, enemies, destructibles
Damageamount, on_hit_effectBullets, hazards
Lifetimeremaining_framesBullets, explosions, pickups
InputHandleraction_mapPlayer only
Emitterpattern_id, fire_timerEnemies, some bosses
PickupEffecteffect_type, valuePowerups

System execution order

Systems must run in a defined order each frame because they modify the same shared data. The standard shmup update order:

// System execution order per frame input_system() // read controller, set velocity on player ai_system() // update enemy state machines, set velocity emitter_system() // fire bullets per pattern, spawn bullet entities movement_system() // apply velocity to position for all movers lifetime_system() // decrement timers, queue expired entities for removal collision_system() // broad phase + narrow phase, queue damage events health_system() // apply queued damage, queue death events pickup_system() // process pickup collisions, apply effects animation_system() // advance sprite frames render_system() // draw all entities with Position + Sprite cleanup_system() // remove entities queued for destruction

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.