okay I got tired of writing the same state machine boilerplate for every enemy AI, player controller, and UI screen in my Godot 4 project. each one had its own slightly different version of the same enter/exit/update loop and it was becoming impossible to maintain without breaking something. so I spent an afternoon making a proper reusable base class and I'm not going back.
two classes: a StateMachine node that owns child State nodes, and a State base class everything extends. the machine auto-discovers its states from children on _ready, handles enter/exit, and passes optional message dicts on transitions, so you can do state_machine.transition_to("Hurt", {"damage": 15, "knockback_dir": Vector2.LEFT}) without needing a signal or a shared variable for every context handoff.
# StateMachine.gd
class_name StateMachine
extends Node
var current_state: State = null
var states: Dictionary = {}
func _ready() -> void:
for child in get_children():
if child is State:
states[child.name] = child
child.state_machine = self
if states.size() > 0:
transition_to(states.values()[0].name)
func _process(delta: float) -> void:
if current_state:
current_state.update(delta)
func _physics_process(delta: float) -> void:
if current_state:
current_state.physics_update(delta)
func transition_to(target_name: String, msg: Dictionary = {}) -> void:
if not states.has(target_name):
push_error("StateMachine: unknown state '%s'" % target_name)
return
if current_state:
current_state.exit()
current_state = states[target_name]
current_state.enter(msg)
# State.gd
class_name State
extends Node
var state_machine: StateMachine = null
func enter(_msg: Dictionary = {}) -> void:
pass
func exit() -> void:
pass
func update(_delta: float) -> void:
pass
func physics_update(_delta: float) -> void:
pass
usage ends up being pretty clean: make a node tree like Enemy > StateMachine > Idle, Chase, Attack, Hurt, each state script extends State and overrides whatever it needs. the message dict has honestly been the best part. instead of setting flags on the parent or using an autoload to pass context between states, everything stays local to the transition call.
the one thing I haven't solved cleanly: sub-states. if I need an Attack state that has its own windup/active/recovery phases, I'm currently nesting a second StateMachine node as a child of the Attack state node. works fine but feels a bit gross. curious if anyone has a cleaner pattern for hierarchical states in Godot without it ballooning into something unreadable.