mcpField
S
state-machine

state-machine

github.com/jame581开发929197 次安装2 周前更新
代码仓库

关于 state-machine

state-machine 是面向 Godot 4.3+ 的有限状态机技能,提供 GDScript 和 C# 模式说明。它帮助 Godot 开发者根据项目复杂度,在枚举、节点、资源、层级和并行设计间做选择。

主要功能

  1. 01比较三种核心 FSM 方案
  2. 02提供 GDScript 和 C# 示例
  3. 03介绍层级与并行模式
  4. 04提供状态转换与生命周期清单

适用场景

  1. 01为角色行为选择状态机模式
  2. 02设计可在编辑器配置的 AI 状态
  3. 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
  • 并行状态机不会修改同一状态(例如都设置速度)——每个状态机只负责一个关注点

评论

支持 Markdown