Finite State Machines in Godot

A finite state machine constrains an object to exactly one of a fixed set of states at a time, with explicit rules for what triggers a switch between them. In Godot the typical candidates are character controllers, enemy AI, and multi-step UI flows — anything with more than three or four mutually exclusive behaviors and conditional branching between them. Below that threshold, a couple of booleans is simpler and the FSM machinery is overhead you don't need.

Godot doesn't impose one FSM implementation. The four below trade off editor visibility, reusability, and runtime cost differently; pick based on state count and how often the graph changes.

APPROACH COMPARISON
ApproachBest forEditor visibilityRuntime cost
Enum + matchSmall, fixed state sets; hot loopsNone — code onlyLowest
Node-based State patternReusable per-actor states with enter/exit logicFull — scene treeLow
AnimationTree state machineStates tied directly to animation playbackFull — visual graph editorLow
Hierarchical (nested)Controllers with grouped behaviors (grounded/airborne)Full — nested scenesLow
Data-driven transition tableMany event-driven transitions, externally configurablePartial — data, not visualLow

01Enum + match

The flat approach: an enum, a current-state variable, a match in _physics_process. No node overhead, no abstraction — appropriate when the state count is fixed and unlikely to grow.

player.gd
Pitfall

There's no enter()/exit() hook, so transient state — jump count, coyote-time timers, hit invincibility — has to be reset manually at every code path that assigns state. Forgetting one path is the most common bug in this style.

02Node-based State pattern

Once states need their own data and their own enter/exit logic, or get reused across multiple actors, model each state as a Node with a shared base class. States become children of a StateMachine node — visible and reorderable in the scene tree.

state.gd
state_machine.gd
states/run_state.gd
Note

The StateMachine indexes states by child.name, so naming a child node Run in the editor is the state's identifier — renaming the node renames the state, and transitioned.emit("Run") has to match it exactly.

03AnimationTree's built-in state machine

Godot ships a visual FSM editor on the AnimationTree node: an AnimationNodeStateMachine root where states are AnimationNodeAnimation nodes (or nested sub-state-machines) wired together with transition arrows drawn in the editor. travel() walks that graph instead of jumping directly to a target.

player.gd
Warning

travel() only moves along transition arrows you've drawn in the editor. If no path exists between the current and target state it stays put rather than jumping directly — it doesn't fall back to an instant switch unless the transition is marked for immediate advance. Call playback.get_current_node() while debugging to confirm where it actually landed.

04Hierarchical (nested) state machines

Real controllers usually need nesting: Grounded and Airborne as top-level states, each owning its own sub-states — Idle/Run under Grounded, Jump/Fall under Airborne. Implement this by composition: a state node embeds its own child StateMachine and forwards the per-frame calls down to it.

states/grounded_state.gd
Pitfall

The outer transition must call exit() on the active sub-state, not just the outer state — otherwise timers, signal connections, or tweens started by the sub-state leak across the outer transition and keep firing after Airborne takes over.

05Data-driven transitions

For states with many event-driven transitions, hardcoding transitioned.emit("X") calls throughout state bodies scales poorly. A transition table — keyed by state name, then event name — separates the graph from the behavior code and can be logged, inspected, or loaded from an external file.

state_machine.gd (excerpt)

States call fire_event("jump") instead of naming a destination directly. Invalid transitions for the current state are silently ignored, which doubles as input validation — pressing jump while airborne is a no-op because Jump/Fall rows don't define a jump key.

06Performance notes

07Common pitfalls