Skills/godot-state-management/SKILL.md

7.4 KiB

name description license compatibility metadata
godot-state-management State management patterns for Godot 4.x: enum-based state, finite state machines (FSM), hierarchical FSM, and behavior trees for AI. Use when implementing character states, game flow, menu systems, or AI decision-making in Godot. MIT
godot-4.0
godot-4.1
godot-4.2
godot-4.3
godot-4.4
godot-4.5
godot-4.6
godot-4.7
author version tags created
vl4dt 0.1.0
godot
state-machine
fsm
behavior-tree
ai
2026-06-21

State Management in Godot

Patterns for managing game and entity states cleanly.

Enum-Based State (Simple)

For straightforward state tracking without transitions:

class_name PlayerController extends CharacterBody2D

enum State { IDLE, RUNNING, JUMPING, FALLING, ATTACKING }

var current_state: State = State.IDLE
var velocity: Vector2

func _physics_process(delta: float) -> void:
    match current_state:
        State.IDLE:
            _handle_idle(delta)
        State.RUNNING:
            _handle_running(delta)
        State.JUMPING:
            _handle_jumping(delta)
        State.FALLING:
            _handle_falling(delta)
        State.ATTACKING:
            _handle_attacking(delta)

func set_state(new_state: State) -> void:
    if current_state == new_state:
        return
    current_state = new_state

Finite State Machine (FSM)

For entities with defined transitions between states:

class_name FSM extends Node

signal state_entered(state_name: StringName)
signal state_exited(state_name: StringName)

var current_state: FSMState = null
var states: Dictionary = {}

func _ready() -> void:
    for child in get_children():
        if child is FSMState:
            states[child.name] = child

func initialize(initial: StringName) -> void:
    if not states.has(initial):
        push_error("FSM: Initial state '%s' not found" % initial)
        return
    current_state = states[initial]
    current_state.enter()
    state_entered.emit(initial)

func change_state(new_state_name: StringName) -> void:
    if not states.has(new_state_name):
        push_error("FSM: State '%s' not found" % new_state_name)
        return
    if current_state:
        current_state.exit()
        state_exited.emit(current_state.name)
    current_state = states[new_state_name]
    current_state.enter()
    state_entered.emit(new_state_name)

func _process(delta: float) -> void:
    if current_state:
        current_state.update(delta)

func _unhandled_input(event: InputEvent) -> void:
    if current_state:
        current_state.handle_input(event)

FSM State Base Class

class_name FSMState extends Node

func enter() -> void:
    pass  # Override

func exit() -> void:
    pass  # Override

func update(delta: float) -> void:
    pass  # Override

func handle_input(event: InputEvent) -> void:
    pass  # Override

# Helper to transition from within a state
func request_state(fsm: FSM, new_state: StringName) -> void:
    fsm.change_state(new_state)

Concrete State Example

# EnemyIdle.gd extends FSMState
class_name EnemyIdle extends FSMState

@export var idle_timer: float = 2.0
var _timer: float = 0.0

func enter() -> void:
    _timer = 0.0

func update(delta: float) -> void:
    _timer += delta
    if _timer >= idle_timer:
        get_parent().request_state(get_parent().get_node("FSM"), "Patrol")

Hierarchical State Machine

For nested states (e.g., combat has sub-states for melee/range):

class_name HierarchicalFSM extends Node

var current_parent: StringName = ""
var current_child: StringName = ""

# Parent states: "Exploring", "Combat", "Dead"
# Combat child states: "Melee", "Ranged", "Dodging"

func enter_state(parent: StringName, child: StringName) -> void:
    current_parent = parent
    current_child = child

Hierarchical State Pattern

# PlayerCombatState.gd — parent state delegates to children
class_name PlayerCombatState extends FSMState

var sub_states: Dictionary = {}
var active_sub_state: StringName = ""

func enter() -> void:
    # Register sub-states from child nodes
    for child in get_children():
        if child is FSMState:
            sub_states[child.name] = child

func update(delta: float) -> void:
    # Choose sub-state based on context
    var new_sub_state = _determine_sub_state()
    if new_sub_state != active_sub_state:
        _switch_sub_state(new_sub_state)
    if sub_states.has(active_sub_state):
        sub_states[active_sub_state].update(delta)

func _determine_sub_state() -> StringName:
    # Example: switch based on weapon type or enemy distance
    return "Melee"

Behavior Trees (AI)

For complex AI decision-making:

class_name BehaviorTree extends Node

var root_node: BTNode = null

func tick(delta: float, blackboard: Dictionary) -> BTNode.Status:
    if not root_node:
        root_node = get_child(0)
    return root_node.execute(blackboard, delta)

# Blackboard — shared data between nodes
# { "target": Node, "health": int, "last_seen_pos": Vector2 }

BT Node Types

class_name BTNode extends Node

enum Status { SUCCESS, FAILURE, RUNNING }

func execute(blackboard: Dictionary, delta: float) -> Status:
    return Status.SUCCESS  # Override in subclasses

# --- Composite Nodes ---

class_name Sequence extends BTNode
func execute(bb: Dictionary, delta: float) -> Status:
    for child in get_children():
        if child is BTNode:
            var result = child.execute(bb, delta)
            if result != Status.SUCCESS:
                return result
    return Status.SUCCESS

class_name Selector extends BTNode
func execute(bb: Dictionary, delta: float) -> Status:
    for child in get_children():
        if child is BTNode:
            var result = child.execute(bb, delta)
            if result == Status.SUCCESS:
                return Status.SUCCESS
    return Status.FAILURE

# --- Leaf Nodes ---

class_name Condition extends BTNode
func check(blackboard: Dictionary) -> bool:
    return true  # Override

func execute(bb: Dictionary, _delta: float) -> Status:
    return Status.SUCCESS if check(bb) else Status.FAILURE

class_name Action extends BTNode
@export var duration: float = 0.0
var elapsed: float = 0.0

func do_action(blackboard: Dictionary) -> void:
    pass  # Override

func execute(bb: Dictionary, delta: float) -> Status:
    if duration > 0:
        elapsed += delta
        do_action(bb)
        return Status.SUCCESS if elapsed >= duration else Status.RUNNING
    else:
        do_action(bb)
        return Status.SUCCESS

Godot 4.7 State Notes

  • tween_await() — Use for state transitions with animated duration: await get_tree().create_tween().tween_property(...) .finished
  • AnimationTree — Combine FSM logic with AnimationTree state machines for visual feedback

MCP Bridge Tools (Optional — Live Editor Integration)

When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:

  • scene_tree — Inspect scene hierarchy to audit FSM node structure
  • script_read / script_edit — Read and modify state machine scripts

Note: MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.

References