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

Shmup Design · June 28, 2026

Enemy AI State Machines in Shmups: Designing Readable Adaptive Behavior

Shmup enemies are not required to be intelligent. They are required to be legible, learnable, and threatening in a way the player can eventually understand. Finite state machines are the tool that makes all three of those qualities achievable simultaneously, because they make enemy behavior explicit, bounded, and easy to reason about from both the designer's side and the player's.

An enemy in a shmup has a job: create pressure. How it does that — which path it follows, what it fires, when it changes behavior — determines whether encounters feel hand-crafted and fair or arbitrary and cheap. The technical structure behind enemy behavior does not have to be sophisticated, but it must be clear. A finite state machine (FSM) is the most common architecture for shmup enemy behavior for good reason: it forces the designer to enumerate every state the enemy can be in and every transition between them, which makes designing and debugging behavior far more tractable than ad-hoc conditional logic.

What a state represents

A state in an FSM represents a discrete behavioral mode: what the enemy is doing right now, in terms of movement and firing. Common states for a midrange shmup enemy include an entry state (the enemy enters the screen and moves to its initial position), an active state (the enemy executes its main attack pattern), a transition state (the enemy repositions between attack phases), and a death state (the enemy plays its death animation and removes itself from the scene).

Each state owns its own update logic. The entry state moves the ship to a target position and fires a transition to active when it arrives. The active state runs the attack pattern loop and transitions to a reposition state at the end of each cycle. The reposition state moves to a new screen location and transitions back to active. The death state runs the explosion animation and queues entity removal. None of these states know about each other except through the named transitions; the FSM itself manages which state is current and routes transitions.

// Minimal FSM update dispatch match self.state: case State.ENTRY: self._update_entry(dt) case State.ACTIVE: self._update_active(dt) case State.REPOSITION: self._update_reposition(dt) case State.DEATH: self._update_death(dt)

Transition conditions: what changes state

Transition conditions are the most design-expressive part of an FSM. They determine what the enemy responds to and how. A pure timer transition (state changes after N seconds) produces mechanical, clockwork enemies that are learnable but not adaptive. A player-proximity transition (state changes when the player enters a range) produces reactive enemies that feel aware. A health-threshold transition produces staged bosses that escalate. Most interesting enemies combine all three.

The design principle to apply to transition conditions is the same as for attack tells: every transition the player can meaningfully respond to should be announced before it happens. An enemy that switches from a wide fan attack to a targeted stream should emit a brief visual cue — a color change, a reposition animation, a brief pause — before the stream begins. Without that cue, the player cannot learn to prepare; the transition is a surprise every time, which is frustrating rather than challenging.

Shared state components and enemy families

Most shmup enemy rosters have families: fast scouts that strafe and fire light volleys, heavy units that move slowly and fire dense patterns, support types that do not fire but spawn smaller enemies. Within a family, enemies share state structure and transition logic; what distinguishes them are the parameters — fire rate, pattern geometry, movement speed, health threshold values.

Implementing shared state logic in a base class or component and parameterising it per-enemy type is the right architecture here. A ScoutFSM and a HeavyFSM that both inherit from EnemyFSM share the entry, reposition, and death states and override only the active state behaviour and the specific transition thresholds. This dramatically reduces the code surface for adding new enemy types and ensures consistent transition behaviour across the roster.

Designing the active state: attack pattern execution

The active state is where the enemy's identity lives. It may run a single repeating pattern, cycle through a sequence of patterns, or select patterns based on player position. For most rank-and-file enemies, a single looping pattern is enough: it is learnable in one or two encounters, and variation across the enemy roster provides overall diversity.

For minibosses and elite enemy types, a sequenced active state — where the enemy cycles through patterns A, B, and C in order before repeating — creates a more encounter-specific feel without the complexity of full boss phase architecture. The player learns the sequence over a few encounters, can anticipate the pattern change, and feels genuinely skilled for doing so. The key constraint is that the sequence must be deterministic and loop consistently; any randomisation should be in parameters (e.g., the exact angle of a aimed shot) not in the sequence order itself.

Debugging FSM behavior

FSMs are significantly easier to debug than ad-hoc AI because the state is always explicit. Adding a debug overlay that renders the current state name and transition timer above each active enemy costs almost nothing to implement and makes AI bugs instantly diagnosable. An enemy that refuses to fire is stuck in its entry state; an enemy that fires too fast is transitioning to active before the timer expires; an enemy that never repositions has a missing transition condition. All of these are visible the moment state is rendered.

Keep a log of state transitions during development runs. The log format is simple: entity ID, timestamp, previous state, new state, transition trigger. Review it after anomalous encounters to trace exactly what the enemy was doing and when. Shmup AI bugs are almost always logic errors in transition conditions, not in the state update logic itself, and the transition log makes them fast to isolate.