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 | Best for | Editor visibility | Runtime cost |
|---|---|---|---|
| Enum + match | Small, fixed state sets; hot loops | None — code only | Lowest |
| Node-based State pattern | Reusable per-actor states with enter/exit logic | Full — scene tree | Low |
| AnimationTree state machine | States tied directly to animation playback | Full — visual graph editor | Low |
| Hierarchical (nested) | Controllers with grouped behaviors (grounded/airborne) | Full — nested scenes | Low |
| Data-driven transition table | Many event-driven transitions, externally configurable | Partial — data, not visual | Low |
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.
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.
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.
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.
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.
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
- String vs StringName: equality on
StringNameis a pointer comparison after interning, materially cheaper thanStringequality once you're running hundreds of AI agents through the same checks. - Enum + match compiles to a jump table and stays the fastest option when the state set is fixed at compile time — keep it for hot paths like bullets or particle controllers, not full character controllers.
- Inactive states shouldn't run: only the active state should do per-frame work. With the Node-based pattern, route
_process/_physics_processthrough theStateMachinerather than letting everyStatenode run its own process callbacks — otherwise inactive states still get ticked by the engine even though their output is discarded.
07Common pitfalls
- Asymmetric enter/exit. Anything acquired in
enter()— timers started, signals connected, areas set to monitoring — has to be released inexit(), even for states that "shouldn't" need cleanup. The cost of skipping it only shows up after dozens of transitions, as a slow signal-connection leak. - Querying physics state before
move_and_slide().is_on_floor()reflects the result of the previousmove_and_slide()call. Decide transitions after movement is applied for the frame, not before, or landing detection lags by one physics tick. - Re-entrant transitions. If a state's
exit()can itself trigger a transition (a cleanup routine deciding to redirect), it can recurse into_on_transitionedbefore the original transition has finished unwinding. Guard with a simpleis_transitioningflag if any state logic can trigger further transitions.