关于 state-machine
state-machine 是面向 Godot 4.3+ 的有限状态机技能,提供 GDScript 和 C# 模式说明。它帮助 Godot 开发者根据项目复杂度,在枚举、节点、资源、层级和并行设计间做选择。
主要功能
- 01比较三种核心 FSM 方案
- 02提供 GDScript 和 C# 示例
- 03介绍层级与并行模式
- 04提供状态转换与生命周期清单
适用场景
- 01为角色行为选择状态机模式
- 02设计可在编辑器配置的 AI 状态
- 03按不同关注点拆分大型 FSM
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill state-machine安装后在 Claude Code 中输入/state-machine确认已加载。
附带文件references/hierarchical-and-parallel.md
详细介绍
Godot 4.3+ 中的状态机
根据复杂程度选择合适的 FSM 模式。所有示例均面向 Godot 4.3+,不使用已弃用的 API。
相关技能: player-controller 用于集成移动状态,ai-navigation 用于 AI 状态模式,resource-pattern 用于基于资源的状态配置,animation-system 用于由 FSM 驱动的 AnimationTree 状态,dialogue-system 用于将对话流程建模为状态机,ability-system 用于施法者状态门控(施法/眩晕),如果需要在 FSM 之外使用行为树,可查看 limboai 插件的 HSM(
BTState),以及仅使用 GDScript 的 BT 替代方案 beehave。
何时选择插件: 本技能介绍内置的 FSM 模式(枚举、基于节点、基于资源)。如果你的 Agent 需要完整的行为树,请改用 limboai(C++ + HSM,Godot 4.6+)或 beehave(纯 GDScript,Godot 4.1+)。
1. 模式对比
| 模式 | 复杂度 | 最适合 |
|---|---|---|
| 基于枚举 | 低 | 状态少于 5 个的简单对象 |
| 基于节点 | 中 | 行为复杂的角色 |
| 基于资源 | 高 | 数据驱动或可在编辑器中配置的 AI |
推荐某种模式时,应说明你排除的模式名称,以及作出选择的决定因素(状态数量、独立关注点、设计师编辑需求)——例如:“选择基于节点的模式,而不是使用 match 的枚举模式,因为 7 个状态各自都有进入/退出逻辑,已经不适合放在一个脚本中”。如果没有明确指出替代方案,就无法驳回该选择。
2. 模式 1:基于枚举(最简单)
状态数量较少,且没有重要的进入/退出逻辑时使用。
GDScript
extends CharacterBody2D
enum State { IDLE, PATROL, CHASE, ATTACK }
@export var patrol_range: float = 200.0
@export var chase_range: float = 300.0
@export var attack_range: float = 50.0
@export var speed: float = 80.0
var current_state: State = State.IDLE
var patrol_target: Vector2 = Vector2.ZERO
@onready var player: Node2D = get_tree().get_first_node_in_group("player")
func _physics_process(delta: float) -> void:
match current_state:
State.IDLE:
_state_idle()
State.PATROL:
_state_patrol()
State.CHASE:
_state_chase()
State.ATTACK:
_state_attack()
move_and_slide()
func _state_idle() -> void:
velocity = Vector2.ZERO
if _player_in_range(chase_range):
current_state = State.CHASE
elif randf() < 0.005:
patrol_target = global_position + Vector2(randf_range(-patrol_range, patrol_range), 0.0)
current_state = State.PATROL
func _state_patrol() -> void:
var direction := (patrol_target - global_position)
if direction.length() < 4.0:
current_state = State.IDLE
return
velocity = direction.normalized() * speed
if _player_in_range(chase_range):
current_state = State.CHASE
func _state_chase() -> void:
if not is_instance_valid(player):
current_state = State.IDLE
return
if _player_in_range(attack_range):
current_state = State.ATTACK
return
if not _player_in_range(chase_range):
current_state = State.PATROL
return
velocity = (player.global_position - global_position).normalized() * speed
func _state_attack() -> void:
velocity = Vector2.ZERO
if not _player_in_range(attack_range):
current_state = State.CHASE
func _player_in_range(range: float) -> bool:
if not is_instance_valid(player):
return false
return global_position.distance_to(player.global_position) <= range
C# 等效实现
using Godot;
public partial class SimpleEnemy : CharacterBody2D
{
private enum State { Idle, Patrol, Chase, Attack }
[Export] public float PatrolRange { get; set; } = 200f;
[Export] public float ChaseRange { get; set; } = 300f;
[Export] public float AttackRange { get; set; } = 50f;
[Export] public float Speed { get; set; } = 80f;
private State _currentState = State.Idle;
private Vector2 _patrolTarget = Vector2.Zero;
private Node2D _player;
public override void _Ready()
{
_player = GetTree().GetFirstNodeInGroup("player") as Node2D;
}
public override void _PhysicsProcess(double delta)
{
switch (_currentState)
{
case State.Idle: StateIdle(); break;
case State.Patrol: StatePatrol(); break;
case State.Chase: StateChase(); break;
case State.Attack: StateAttack(); break;
}
MoveAndSlide();
}
private void StateIdle()
{
Velocity = Vector2.Zero;
if (PlayerInRange(ChaseRange))
{
_currentState = State.Chase;
}
else if (GD.Randf() < 0.005f)
{
_patrolTarget = GlobalPosition + new Vector2((float)GD.RandRange(-PatrolRange, PatrolRange), 0f);
_currentState = State.Patrol;
}
}
private void StatePatrol()
{
var direction = _patrolTarget - GlobalPosition;
if (direction.Length() < 4f) { _currentState = State.Idle; return; }
Velocity = direction.Normalized() * Speed;
if (PlayerInRange(ChaseRange)) _currentState = State.Chase;
}
private void StateChase()
{
if (!IsInstanceValid(_player)) { _currentState = State.Idle; return; }
if (PlayerInRange(AttackRange)) { _currentState = State.Attack; return; }
if (!PlayerInRange(ChaseRange)) { _currentState = State.Patrol; return; }
Velocity = (_player.GlobalPosition - GlobalPosition).Normalized() * Speed;
}
private void StateAttack()
{
Velocity = Vector2.Zero;
if (!PlayerInRange(AttackRange)) _currentState = State.Chase;
}
private bool PlayerInRange(float range) =>
IsInstanceValid(_player) && GlobalPosition.DistanceTo(_player.GlobalPosition) <= range;
}
何时应从基于枚举的模式升级:
- 进入/退出逻辑开始在各个状态方法中重复
- 动画同步需要显式的进入/退出钩子
match/switch代码块增长到约 100 行以上
3. 模式 2:基于节点(推荐用于角色)
每个状态都是独立的节点。StateMachine 节点会将输入和处理调用委托给当前活动状态,状态则按名称触发转换。
场景树
Player (CharacterBody2D)
└── StateMachine (Node)
├── Idle (State)
├── Run (State)
├── Jump (State)
└── Attack (State)
状态基类
GDScript(state.gd)
class_name State
extends Node
## Populated by StateMachine._ready()
var entity: CharacterBody2D
var state_machine: StateMachine
## Called when this state becomes active.
func enter() -> void:
pass
## Called when this state is deactivated.
func exit() -> void:
pass
## Mirrors _process. Return a state name string to transition, or "" to stay.
func update(delta: float) -> String:
return ""
## Mirrors _physics_process. Return a state name string to transition, or "".
func physics_update(delta: float) -> String:
return ""
## Mirrors _unhandled_input.
func handle_input(event: InputEvent) -> String:
return ""
C#(State.cs)
using Godot;
public partial class State : Node
{
/// Populated by StateMachine._Ready()
public CharacterBody2D Entity { get; set; }
public StateMachine StateMachine { get; set; }
public virtual void Enter() { }
public virtual void Exit() { }
public virtual string Update(double delta) => string.Empty;
public virtual string PhysicsUpdate(double delta) => string.Empty;
public virtual string HandleInput(InputEvent @event) => string.Empty;
}
StateMachine 类
GDScript(state_machine.gd)
class_name StateMachine
extends Node
@export var initial_state: State
var current_state: State
var states: Dictionary = {}
func _ready() -> void:
for child in get_children():
if child is State:
states[child.name] = child
child.entity = owner as CharacterBody2D
child.state_machine = self
if initial_state:
current_state = initial_state
current_state.enter()
func _unhandled_input(event: InputEvent) -> void:
var next := current_state.handle_input(event)
if next:
transition_to(next)
func _process(delta: float) -> void:
var next := current_state.update(delta)
if next:
transition_to(next)
func _physics_process(delta: float) -> void:
var next := current_state.physics_update(delta)
if next:
transition_to(next)
func transition_to(state_name: String) -> void:
if not states.has(state_name):
push_error("StateMachine: unknown state '%s'" % state_name)
return
current_state.exit()
current_state = states[state_name]
current_state.enter()
C#(StateMachine.cs)
using System.Collections.Generic;
using Godot;
public partial class StateMachine : Node
{
[Export] public State InitialState { get; set; }
public State CurrentState { get; private set; }
private readonly Dictionary<string, State> _states = new();
public override void _Ready()
{
foreach (var child in GetChildren())
{
if (child is State state)
{
_states[state.Name] = state;
state.Entity = Owner as CharacterBody2D;
state.StateMachine = this;
}
}
if (InitialState != null)
{
CurrentState = InitialState;
CurrentState.Enter();
}
}
public override void _UnhandledInput(InputEvent @event)
{
var next = CurrentState.HandleInput(@event);
if (!string.IsNullOrEmpty(next)) TransitionTo(next);
}
public override void _Process(double delta)
{
var next = CurrentState.Update(delta);
if (!string.IsNullOrEmpty(next)) TransitionTo(next);
}
public override void _PhysicsProcess(double delta)
{
var next = CurrentState.PhysicsUpdate(delta);
if (!string.IsNullOrEmpty(next)) TransitionTo(next);
}
public void TransitionTo(string stateName)
{
if (!_states.TryGetValue(stateName, out var next))
{
GD.PushError($"StateMachine: unknown state '{stateName}'");
return;
}
CurrentState.Exit();
CurrentState = next;
CurrentState.Enter();
}
}
具体示例:IdleState
GDScript(idle_state.gd)
class_name IdleState
extends State
func enter() -> void:
entity.get_node("AnimationPlayer").play("idle")
func physics_update(delta: float) -> String:
if not entity.is_on_floor():
return "Jump"
if Input.get_axis("move_left", "move_right") != 0.0:
return "Run"
return ""
func handle_input(event: InputEvent) -> String:
if event.is_action_pressed("jump") and entity.is_on_floor():
return "Jump"
if event.is_action_pressed("attack"):
return "Attack"
return ""
4. 模式 3:基于资源(数据驱动)
需要设计师在 Godot 检查器中配置状态而无需修改代码时使用。
StateData 资源
class_name StateData
extends Resource
@export var state_name: String = ""
@export var animation_name: String = ""
@export var move_speed: float = 0.0
@export var can_transition_to: Array[String] = []
在 AI 控制器上导出一个 Array[StateData]。设计师可在检查器中填充每个条目,无需修改代码即可调整行为或添加状态。运行时会读取 can_transition_to 来验证状态转换,并为每个活动状态选取 animation_name/move_speed。
using Godot;
[GlobalClass]
public partial class StateData : Resource
{
[Export] public string StateName { get; set; } = string.Empty;
[Export] public string AnimationName { get; set; } = string.Empty;
[Export] public float MoveSpeed { get; set; } = 0f;
[Export] public Godot.Collections.Array<string> CanTransitionTo { get; set; } = new();
}
在 AI 控制器类上添加一个导出的 Array[StateData]([Export] public Godot.Collections.Array<StateData> States)。运行时,根据 StateName 查找活动的 StateData,并读取 AnimationName/MoveSpeed 来驱动行为;使用 CanTransitionTo 为 TransitionTo 调用添加保护。
5. 分层与并行状态机
当扁平 FSM 增长到约 8 个以上的状态,或涉及多个关注点(移动 + 战斗 + 动画)时,可拆分为分层状态机(状态包含子状态机,例如 OnGround 包含 Idle/Walk/Run),或并行状态机(用于移动、战斗、动画的独立 FSM 并行运行)。这两种方式都能让状态数量相加,而非相乘。
请参阅 references/hierarchical-and-parallel.md,了解完整的场景树、HierarchicalState 基类、并行状态机角色示例,以及“如何选择”对比表;以上内容均提供 GDScript 和 C# 版本。
6. 决策流程图
Start
│
▼
Fewer than 5 states?
├─ Yes ──────────────────────────────────► Enum-Based
└─ No
│
▼
Multiple independent concerns
(movement + combat + animation)?
├─ Yes ──────────────────────────────► Parallel State Machines
└─ No
│
▼
States naturally nest
(sub-states within states)?
├─ Yes ────────────────────────► Hierarchical State Machine
└─ No
│
▼
Designers need to configure
states in the Inspector?
├─ Yes ──────────────────► Resource-Based
└─ No ──────────────────► Node-Based
7. 实施检查清单
- 选择了与实际复杂度相匹配的方案(enum / node / resource)
- 说明了被否决的方案以及决定因素
- 每个状态都有显式的
enter()和exit()方法(或等效方法) - 所有转换都有明确命名——状态之间不存在隐式直通
- 在需要时,于
enter()中启动动画,并在exit()中清理动画 - 不存在可能导致单帧内无限递归的循环转换
- 当状态数量超过约 8 个或涉及多个关注点时,将扁平 FSM 替换为层次 FSM 或并行 FSM
- 并行状态机不会修改同一状态(例如都设置速度)——每个状态机只负责一个关注点