State machines are one of the most important patterns in game development. They keep your code organized, prevent impossible states, and make complex behaviors manageable. In this tutorial, we'll build from simple enum-based FSMs to full enemy AI.
What is a State Machine?
A finite state machine (FSM) is a pattern where an entity can only be in one state at a time. A player might be IDLE, WALKING, JUMPING, or ATTACKING—but never two at once. Transitions happen based on conditions (pressed jump, touched ground, etc.).
Simple Enum-Based State Machine
For simple cases with 3-5 states, an enum-based approach is clean and straightforward.
extends CharacterBody2D
enum State { IDLE, WALK, JUMP, FALL }
var current_state: State = State.IDLE
var speed = 200.0
var jump_force = -400.0
var gravity = 980.0
func _physics_process(delta):
# Apply gravity
velocity.y += gravity * delta
# Get input
var direction = Input.get_axis("move_left", "move_right")
# State machine
match current_state:
State.IDLE:
velocity.x = 0
if direction != 0:
change_state(State.WALK)
if Input.is_action_just_pressed("jump") and is_on_floor():
change_state(State.JUMP)
State.WALK:
velocity.x = direction * speed
if direction == 0:
change_state(State.IDLE)
if Input.is_action_just_pressed("jump") and is_on_floor():
change_state(State.JUMP)
if not is_on_floor():
change_state(State.FALL)
State.JUMP:
velocity.x = direction * speed
if velocity.y > 0:
change_state(State.FALL)
State.FALL:
velocity.x = direction * speed
if is_on_floor():
change_state(State.IDLE if direction == 0 else State.WALK)
move_and_slide()
func change_state(new_state: State):
# Exit current state
match current_state:
State.JUMP:
pass # Could stop jump animation
current_state = new_state
# Enter new state
match new_state:
State.JUMP:
velocity.y = jump_force
State.IDLE:
$AnimationPlayer.play("idle")
State.WALK:
$AnimationPlayer.play("walk")Node-Based State Machine Pattern
For complex AI or when states have their own variables and logic, a node-based approach is more maintainable. Each state is a separate node with its own script.
# state_machine.gd - Attach to a Node called StateMachine
extends Node
class_name StateMachine
@export var initial_state: State
var current_state: State
func _ready():
# Initialize all state children
for child in get_children():
if child is State:
child.state_machine = self
child.process_mode = Node.PROCESS_MODE_DISABLED
# Start initial state
if initial_state:
change_state(initial_state)
func _process(delta):
if current_state:
current_state.update(delta)
func _physics_process(delta):
if current_state:
current_state.physics_update(delta)
func change_state(new_state: State):
if current_state:
current_state.exit()
current_state.process_mode = Node.PROCESS_MODE_DISABLED
current_state = new_state
current_state.process_mode = Node.PROCESS_MODE_INHERIT
current_state.enter()# state.gd - Base class for all states
extends Node
class_name State
var state_machine: StateMachine
func enter() -> void:
pass
func exit() -> void:
pass
func update(_delta: float) -> void:
pass
func physics_update(_delta: float) -> void:
pass// States defined in Animator Controller
// Transitions via parameters
animator.SetBool("isWalking", true);
animator.SetTrigger("attack");# States in code or as nodes
# Direct control over transitions
state_machine.change_state(walk_state)
state_machine.change_state(attack_state)Enemy AI States
Let's implement a classic enemy AI with Idle, Patrol, Chase, and Attack states.
# enemy_idle_state.gd
extends State
@export var idle_time: float = 2.0
var timer: float = 0.0
func enter():
timer = idle_time
get_parent().get_parent().velocity = Vector2.ZERO # Stop moving
func update(delta):
timer -= delta
if timer <= 0:
state_machine.change_state($"../Patrol")
# Check for player
var enemy = get_parent().get_parent()
if enemy.can_see_player():
state_machine.change_state($"../Chase")# enemy_patrol_state.gd
extends State
@export var patrol_speed: float = 100.0
var direction: int = 1
func enter():
direction = 1 if randf() > 0.5 else -1
func physics_update(delta):
var enemy = get_parent().get_parent()
enemy.velocity.x = direction * patrol_speed
# Turn at walls or edges
if enemy.is_on_wall() or not enemy.is_on_floor_ahead():
direction *= -1
enemy.move_and_slide()
# Check for player
if enemy.can_see_player():
state_machine.change_state($"../Chase")# enemy_chase_state.gd
extends State
@export var chase_speed: float = 150.0
@export var attack_range: float = 50.0
func physics_update(delta):
var enemy = get_parent().get_parent()
var player = enemy.player_ref
if not player:
state_machine.change_state($"../Idle")
return
# Move toward player
var direction = sign(player.position.x - enemy.position.x)
enemy.velocity.x = direction * chase_speed
enemy.move_and_slide()
# Attack if close enough
var distance = enemy.position.distance_to(player.position)
if distance < attack_range:
state_machine.change_state($"../Attack")
# Lost sight of player
if not enemy.can_see_player():
state_machine.change_state($"../Idle")Adding Pathfinding with NavigationAgent2D
For smarter AI that navigates around obstacles, use Godot's built-in navigation system.
# enemy_chase_state.gd (with navigation)
extends State
@export var chase_speed: float = 150.0
@onready var nav_agent: NavigationAgent2D = get_parent().get_parent().get_node("NavigationAgent2D")
func enter():
nav_agent.path_desired_distance = 4.0
nav_agent.target_desired_distance = 4.0
func physics_update(delta):
var enemy = get_parent().get_parent()
var player = enemy.player_ref
if not player:
state_machine.change_state($"../Idle")
return
# Update navigation target
nav_agent.target_position = player.global_position
if nav_agent.is_navigation_finished():
state_machine.change_state($"../Attack")
return
# Get next point on path
var next_pos = nav_agent.get_next_path_position()
var direction = enemy.global_position.direction_to(next_pos)
enemy.velocity = direction * chase_speed
enemy.move_and_slide()When to Use AnimationTree Instead
AnimationTree has its own StateMachine node type. Use it when your states are primarily about animation blending (like character locomotion with walk/run/sprint). Use code-based FSMs when states involve complex game logic beyond animation.
Key Takeaways
- Start with enum-based FSMs for simple behaviors
- Graduate to node-based when states become complex
- Use signals to decouple state machines from other systems
- NavigationAgent2D handles pathfinding automatically
- Each state should handle its own enter/exit/update logic
