State machines are everywhere in games once you start looking. A player that idles, runs, jumps, and attacks? State machine. An enemy that patrols, chases, and fights? State machine. A door that's locked, unlocked, opening, and open? Also a state machine, even if nobody bothered to call it one.
I avoided this pattern for longer than I'd like to admit because it sounded like enterprise architecture homework. Then a player controller hit around 300 lines of tangled booleans and I caved. I haven't written a character without one since.
What Is a State Machine?
A state machine is a pattern where an object is in exactly one state at a time, and the moves between states follow rules you define.
Your character is idle, or running, or jumping, or attacking. Never two at once. Each state owns its behavior, its animation, and its rules for when to hand control to another state.
Without one, character code turns into this:
# The "if-else hell" approach - don't do thisfunc _physics_process(delta: float) -> void: if is_attacking: # attack logic... if attack_finished: is_attacking = false elif is_jumping: # jump logic... if is_on_floor(): is_jumping = false elif is_running: # run logic... if Input.is_action_just_pressed("attack"): is_attacking = true is_running = false else: # idle logic... if Input.is_action_just_pressed("jump"): is_jumping = trueThis breaks fast. Add a dash, a wall slide, and a ledge grab, and you're staring at 200 lines of booleans where every new ability makes every old ability buggier.
The Clean Alternative
A state machine replaces the boolean pile with a single current_state variable and a set of state classes, each minding its own business.
Here's the structure:
CharacterBody3D (Player)
├── StateMachine
│ ├── IdleState
│ ├── RunState
│ ├── JumpState
│ └── AttackState
├── AnimationPlayer
└── CollisionShape3D
Building the State Machine in Godot 4
Step 1: The Base State
Every state shares the same interface, so start with a base State class:
# state.gdclass_name Stateextends Node# Reference to the state machine (set by the StateMachine)var state_machine: StateMachine# Called when entering this statefunc enter() -> void: pass# Called when leaving this statefunc exit() -> void: pass# Called every physics frame while this state is activefunc physics_update(delta: float) -> void: pass# Called every frame while this state is activefunc update(delta: float) -> void: passStep 2: The State Machine
The state machine tracks which state is active and handles the handoffs:
# state_machine.gdclass_name StateMachineextends Node@export var initial_state: Statevar current_state: Statefunc _ready() -> void: # Give each child state a reference back to this machine for child in get_children(): if child is State: child.state_machine = self # Start in the initial state if initial_state: current_state = initial_state current_state.enter()func _physics_process(delta: float) -> void: if current_state: current_state.physics_update(delta)func _process(delta: float) -> void: if current_state: current_state.update(delta)func transition_to(target_state: State) -> void: if current_state == target_state: return current_state.exit() current_state = target_state current_state.enter()Step 3: Concrete States
Now each state is a short, focused script:
# idle_state.gdextends Statefunc enter() -> void: # Play idle animation when entering this state owner.get_node("AnimationPlayer").play("idle")func physics_update(delta: float) -> void: var player: CharacterBody3D = owner # Transition to Run if there's movement input var input := Input.get_vector("left", "right", "forward", "back") if input.length() > 0.1: state_machine.transition_to($"../RunState") return # Transition to Jump if jump pressed if Input.is_action_just_pressed("jump") and player.is_on_floor(): state_machine.transition_to($"../JumpState") return # Transition to Attack if attack pressed if Input.is_action_just_pressed("attack"): state_machine.transition_to($"../AttackState") return# run_state.gdextends State@export var speed := 5.0func enter() -> void: owner.get_node("AnimationPlayer").play("run")func physics_update(delta: float) -> void: var player: CharacterBody3D = owner var input := Input.get_vector("left", "right", "forward", "back") # Move the player player.velocity.x = input.x * speed player.velocity.z = input.y * speed player.move_and_slide() # Transition back to Idle if no input if input.length() < 0.1: state_machine.transition_to($"../IdleState") return # Can still jump or attack from run state if Input.is_action_just_pressed("jump") and player.is_on_floor(): state_machine.transition_to($"../JumpState") elif Input.is_action_just_pressed("attack"): state_machine.transition_to($"../AttackState")Why a State Machine Beats Booleans
Each state is isolated. The idle state knows nothing about attack logic, the jump state doesn't care about running, and you can rip one out or rewrite it without the others noticing.
Adding behavior gets cheap. Want a dash? Add DashState, define its transitions, done. The first state machine takes maybe an hour to set up. Every state after that is twenty minutes.
Debugging stops being archaeology, too. When something goes wrong, print current_state.name and you know exactly what your character thinks it's doing.
And animations stay synced for free, because each state plays its own animation in enter(). No more fighting animation priority or wondering why the run cycle plays mid-air.
Using a State Machine for Enemy AI
The same pattern runs enemy AI, and this is where it earns its keep:
CharacterBody3D (Enemy)
├── StateMachine
│ ├── PatrolState → walk between waypoints
│ ├── ChaseState → pursue the player
│ ├── AttackState → attack when in range
│ ├── StunnedState → stunned after taking damage
│ └── DeadState → death animation, drop loot
├── NavigationAgent3D
└── DetectionArea
Each state handles one slice of behavior. PatrolState walks between points and hands off to ChaseState when the player enters the detection area. ChaseState follows the player and hands off to AttackState when close enough.
The whole brain reads at a glance, which matters a lot when you're balancing difficulty or cloning the setup for a second enemy type. I went deeper on this side of things in my Godot 4 enemy AI tutorial if AI is the part you're actually here for.
Common State Machine Pitfalls
Circular transitions are the classic. State A transitions to B, B immediately transitions back to A, and your character vibrates between two animations. It usually means two states are reading the same condition with opposite thresholds, so test the edges.
Watch for giant states too. If one state passes 200 lines, it's doing too much. Break it into sub-states or pull out helper functions.
And states will need shared data: health, velocity, node references. Use the owner property to reach the parent node, or pass shared data through the state machine itself. Pick one and stick to it.
Going Further
What we built here is a single-level state machine with simple transitions, and it'll carry a small game just fine. Production games usually want more: hierarchical state machines (a "Grounded" super-state containing Idle, Run, and Crouch), state history so a stunned character goes back to whatever it was doing, smooth animation blending between states, and AI layers like utility scoring or behavior trees on top.
All of that is in the State Machine AI quest: 22 lessons where you build a complete 3D enemy from scratch, with patrol, chase, attack, stagger, and death, plus hierarchical states, navigation, and combat integration.
Start smaller than that, though. Take your messiest character script tonight and split it into three states. You'll feel the difference before you finish the second one.
FAQ
Do I need a state machine for a simple game?
No. If your character has two or three behaviors, a few booleans or one enum with a match statement is fine. The pattern starts paying for itself around four or five states, or the first time two booleans are both true when they shouldn't be.
Should states be nodes or plain classes in GDScript?
Node-based states (each state as a child node, like in this tutorial) are the most common approach in Godot and the easiest to inspect, since you can watch them in the remote scene tree while the game runs. Plain RefCounted classes work too and skip the scene tree overhead. For your first state machine, use nodes.
Is this the same as the AnimationTree state machine?
No. AnimationTree has its own state machine node, but it only handles animation playback and blending. Your gameplay logic still needs its own state machine in code, and plenty of games run both side by side: the code decides what the character does, the AnimationTree decides how it looks doing it.


