State Machines & AI Patterns
31.1 Enum-based state machine
Perfect for small actors:
extends CharacterBody2D
enum State { IDLE, PATROL, CHASE, ATTACK, DEAD }
var state: State = State.IDLE:
set(value):
if state == value: return
_exit_state(state)
state = value
_enter_state(state)
@export var speed := 80.0
var player: Node2D
func _physics_process(delta: float) -> void:
match state:
State.IDLE:
if can_see_player(): state = State.CHASE
State.PATROL:
patrol(delta)
if can_see_player(): state = State.CHASE
State.CHASE:
velocity = global_position.direction_to(player.global_position) * speed
move_and_slide()
if global_position.distance_to(player.global_position) < 24.0:
state = State.ATTACK
elif not can_see_player():
state = State.PATROL
State.ATTACK, State.DEAD:
pass
func _enter_state(s: State) -> void:
match s:
State.ATTACK: $AnimationPlayer.play("attack")
State.DEAD: $AnimationPlayer.play("die")
func _exit_state(_s: State) -> void:
pass31.2 Node-based state machine
For complex actors, each state is a node with its own script:
Scene: Player → StateMachine → Idle, Run, Jump, Fall. Variations: hierarchical state machines (states with sub-machines), pushdown automata (a stack of states for pause/menus).
# state.gd
class_name State
extends Node
signal transition_requested(to_state: StringName)
var actor: CharacterBody2D
func enter(_previous: StringName) -> void: pass
func exit() -> void: pass
func handle_input(_event: InputEvent) -> void: pass
func update(_delta: float) -> void: pass
func physics_update(_delta: float) -> void: pass# state_machine.gd
class_name StateMachine
extends Node
@export var initial_state: State
var current: State
var states: Dictionary = {} # StringName → State
func _ready() -> void:
await owner.ready # ensure the actor is fully ready
for child in get_children():
if child is State:
states[child.name] = child
child.actor = owner
child.transition_requested.connect(_transition)
current = initial_state
current.enter(&"")
func _unhandled_input(event: InputEvent) -> void: current.handle_input(event)
func _process(delta: float) -> void: current.update(delta)
func _physics_process(delta: float) -> void: current.physics_update(delta)
func _transition(to_state: StringName) -> void:
if not states.has(to_state):
push_error("No state " + to_state)
return
var previous := current.name
current.exit()
current = states[to_state]
current.enter(previous)# states/idle.gd
extends State
func enter(_previous: StringName) -> void:
actor.get_node("AnimatedSprite2D").play("idle")
func physics_update(_delta: float) -> void:
if Input.get_axis("move_left", "move_right") != 0.0:
transition_requested.emit(&"Run")
elif Input.is_action_just_pressed("jump"):
transition_requested.emit(&"Jump")31.3 Vision and line of sight
@export var view_distance := 300.0
@export var view_angle_deg := 60.0
func can_see_player() -> bool:
if player == null: return false
var to_player := player.global_position - global_position
if to_player.length() > view_distance: return false
var facing := Vector2.RIGHT.rotated(rotation)
if rad_to_deg(facing.angle_to(to_player)) > view_angle_deg / 2.0 \
or rad_to_deg(facing.angle_to(to_player)) < -view_angle_deg / 2.0:
return false
var query := PhysicsRayQueryParameters2D.create(global_position, player.global_position, 1) # world layer
return get_world_2d().direct_space_state.intersect_ray(query).is_empty()31.4 Steering behaviors
Combine forces (separation, alignment, cohesion) for boids/flocking.
func seek(target: Vector2, max_speed: float) -> Vector2:
return (target - global_position).normalized() * max_speed - velocity
func flee(threat: Vector2, max_speed: float) -> Vector2:
return (global_position - threat).normalized() * max_speed - velocity
func arrive(target: Vector2, max_speed: float, slow_radius: float) -> Vector2:
var offset := target - global_position
var dist := offset.length()
var desired_speed := max_speed * minf(dist / slow_radius, 1.0)
return offset.normalized() * desired_speed - velocity
func _physics_process(delta: float) -> void:
var steering := seek(player.global_position, 200.0).limit_length(400.0 * delta)
velocity = (velocity + steering).limit_length(200.0)
move_and_slide()31.5 Other AI techniques
Finite state machines Enum/node FSMs, AnimationTree state machines Complex, modular decisions
Addons: LimboAI (BTs + HSMs, GDExtension), Beehave Choose the best-scoring action (needs, sims) Score functions in GDScript Planning toward goals NavigationAgent, AStarGrid2D Grids + Packed arrays Utility AI sketch:
func choose_action() -> StringName:
var scores := {
&"eat": (100.0 - hunger) * -1.0 + 100.0,
&"sleep": 100.0 - energy,
&"flee": 200.0 if danger_nearby else 0.0,
}
var best: StringName = &"idle"
var best_score := 10.0
for action in scores:
if scores[action] > best_score:
best = action
best_score = scores[action]
return best