Skills/godot-state-management/SKILL.md

286 lines
7.4 KiB
Markdown

---
name: godot-state-management
description: "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."
license: MIT
compatibility:
- godot-4.0
- godot-4.1
- godot-4.2
- godot-4.3
- godot-4.4
- godot-4.5
- godot-4.6
- godot-4.7
metadata:
author: vl4dt
version: 0.1.0
tags:
- godot
- state-machine
- fsm
- behavior-tree
- ai
created: 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:
```gdscript
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:
```gdscript
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
```gdscript
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
```gdscript
# 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):
```gdscript
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
```gdscript
# 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:
```gdscript
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
```gdscript
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
- [FSM Implementation Guide](references/fsm-guide.md)
- [Behavior Tree Patterns](references/behavior-trees.md)
- [Hierarchical FSM Examples](references/hierarchical-fsm.md)