286 lines
7.4 KiB
Markdown
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)
|