# state_machine_editor **Repository Path**: scitboy/state_machine_editor ## Basic Information - **Project Name**: state_machine_editor - **Description**: Godot 玩家状态机编辑器插件 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-02 - **Last Updated**: 2026-05-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 玩家状态机编辑器 (State Machine Editor) Godot 4.x 可视化状态机插件,支持连线式编辑状态跳转、表达式条件判断、一键生成游戏逻辑。 ## 功能 - **可视化连线编辑**:拖拽输出槽到输入槽创建状态跳转,双击连线编辑条件和执行顺序 - **UUID 持久化**:连线基于唯一 ID,节点重命名不影响已有配置 - **条件方法绑定**:跳转条件指定根节点脚本上的 `bool` 方法名,完全解耦 - **多状态视角**:每个 CharacterState 保存独立的节点布局和跳转配置 - **编辑器集成**:选中 CharacterState 节点,底部面板自动展示状态图 ## 目录结构 ``` addons/state_machine_editor/ ├── plugin.cfg # 插件元数据 ├── StateMachineEditorPlugin.gd # EditorPlugin 入口 ├── state_machine_graph.tscn # 状态图 UI 场景 ├── StateMachineGraph.gd # 加载/保存逻辑 ├── GraphCanvas.gd # 画布绘制和交互 │ └── code/ # 游戏运行时代码 ├── enums/ │ └── player_enums.gd # 移动模式和方向枚举 └── player/ ├── controller/ │ └── play_controller.gd # 玩家移动控制器 ├── data/ │ └── player_data.gd # 玩家属性配置资源 ├── state/ │ ├── state_node.gd # 状态节点基类 │ ├── character_state.gd # 角色状态(生命周期+信号) │ ├── state_machine.gd # 状态机管理 │ └── state_transition.gd # 跳转规则资源 ├── player_character.gd # 玩家角色(组合 StateMachine) └── PlayerCharacter.tscn # 玩家角色预配置场景 ``` ## 快速开始 1. 在 Godot 编辑器中启用插件:**项目设置 → 插件 → 玩家状态机** 2. 拖入 `PlayerCharacter.tscn` 或手动创建节点结构: ``` PlayerCharacter (CharacterBody2D) ├── StateMachine (Node) + state_machine.gd │ ├── Idle (Node) + 继承 CharacterState 的脚本 │ └── Run (Node) + 继承 CharacterState 的脚本 └── CollisionShape2D ... ``` 3. 选中任一 CharacterState 节点 → 底部出现"状态图"面板 4. 从选中节点的输出槽(绿点)拖拽到目标节点的输入槽(黄点)创建连线 5. 双击连线配置条件方法名和执行顺序 6. 点击"保存"持久化配置 ## 状态跳转配置 ### 数据存储 每个 `CharacterState` 保存两套数据: | 字段 | 类型 | 说明 | |------|------|------| | `nodes` | `Array` | 状态图节点布局 `[{"name":String, "pos":Vector2, "uid":String}]` | | `transitions` | `Array[StateTransition]` | 跳转规则列表 | ### StateTransition 字段 | 字段 | 类型 | 说明 | |------|------|------| | `from_uid` | `String` | 源状态 UUID | | `to_uid` | `String` | 目标状态 UUID | | `condition` | `String` | 条件方法名(空=无条件),在局部根节点脚本上查找 | | `order` | `int` | 执行优先级,越小越先判断 | ### 运行时流程 ``` _physics_process 每帧 → current_state.on_physics_process(delta) _check_transition() → 从缓存取当前状态的 transitions(按 order 排序) → 遍历:condition 为空 → 立即切换 → 调用 _root_node.call(condition) → 返回 true → 切换 ``` ### 条件回调查找机制(condition)详解 condition 是一个**方法名字符串**,运行时 `StateMachine` 在**局部根节点脚本**上通过 `call(condition)` 调用该方法,方法必须返回 `bool`。 **局部根节点查找规则**(`_find_local_root()`): ``` StateMachine._ready() └── _root_node = _find_local_root() └── 从 get_parent() 开始向上遍历 ├── 跳过 PlayerCharacter(不是局部根) └── 找到第一个带脚本的祖先 → 返回该节点 ``` 嵌套场景中也能正确找到"本模块的根脚本": ``` main.gd (scene root, func IdleToRun() → bool) ←← _root_node 指向这里 └── PlayerCharacter (跳过) └── StateMachine ├── IdleState (CharacterState) └── RunState (CharacterState) ``` **condition 执行流程**: ``` StateMachine._check_transition() ├── 取缓存:var all_trans = _trans_cache[current_state.uid] ├── for t in all_trans (按 order 升序): │ ├── t.condition == "" → change_state(t.to_uid) 无条件跳转 │ ├── _root_node.has_method(t.condition) → _root_node.call(t.condition) │ │ └── 返回 bool true → change_state(t.to_uid) 条件满足 │ └── 返回 false / 方法不存在 → 跳过,继续下一个 transition └── 所有 transition 都不满足 → 保持当前状态 ``` **完整示例:Idle → Run**: 1)在编辑器状态图中,选中 Idle,拖线到 Run,双击连线配置: ``` 条件方法名 = "ShouldRun" 执行顺序 = 0 ``` 2)在根脚本(如 `main.gd`)中定义方法: ```gdscript # main.gd (场景根脚本) func ShouldRun() -> bool: var input = Input.get_vector("move_left", "move_right", "move_up", "move_down") return input.length() > 0.1 # 有输入就返回 true ``` 3)CharacterState "Idle" 的 `transitions` 数组保存: ```gdscript StateTransition { from_uid = "idle的UUID", to_uid = "run的UUID", condition = "ShouldRun", # 方法名 order = 0 } ``` 4)运行时每帧: ``` 当前状态 Idle → _check_transition() → _trans_cache["idle-uid"] 命中 → 遍历 → t.condition = "ShouldRun" → _root_node.call("ShouldRun") → 调用 main.gd 上的方法 → 有输入 → 返回 true → change_state("run-uid") → 进入 Run 状态 ``` **进阶:多条件示例**: ```gdscript # main.gd func ShouldIdle() -> bool: return Input.get_vector("left","right","up","down").length() < 0.05 func IsOnGround() -> bool: return get_node("PlayerCharacter").is_on_floor() func HealthLow() -> bool: return get_node("PlayerCharacter").player_data.current_health < 20 ``` | 源状态 | 目标状态 | condition | order | 说明 | |--------|---------|-----------|-------|------| | Run | Idle | ShouldIdle | 0 | 先判断是否停 | | Run | Dead | HealthLow | 1 | 后判断是否死 | | Any | Jump | IsOnGround | — | 落地不跳 | **注意**:空字符串 `""` 表示**无条件跳转**——遍历到该 transition 时立即切换,不调用任何方法。通常放在最后(order 最大)作为兜底。 --- ## 信号连接 ### 全部信号清单 | 信号 | 发射类 | 触发链路 | 参数 | |------|--------|---------|------| | `entered` | `CharacterState` | `StateMachine.change_state()` → `new_state.enter()` → `entered.emit()` | 无 | | `exited` | `CharacterState` | `StateMachine.change_state()` → `old_state.exit()` → `exited.emit()` | 无 | | `updated` | `CharacterState` | `StateMachine._process()` → `current_state.on_process(delta)` → `updated.emit(delta)` | `delta: float` | | `physics_updated` | `CharacterState` | `StateMachine._physics_process()` → `on_physics_process(delta)` → `physics_updated.emit(delta)` | `delta: float` | | `state_changed` | `StateMachine` | `change_state(uid)` 成功后 | `old_name: String, new_name: String` | | `direction_changed` | `PlayController` | `_physics_process()` → 朝向枚举值变化时 | `new_direction: int, strength: float` | | `moving` | `PlayController` | `_physics_process()` → 每帧有输入时 | `current_direction: int, strength: float` | ### 信号链路图 **状态切换链**: ``` StateMachine.change_state(uid) ├── old_state.exit() → CharacterState.exited.emit() ├── current_state = new_state ├── new_state.enter() → CharacterState.entered.emit() └── state_changed.emit(old, new) └── PlayerCharacter._on_state_changed(old, new) └── 子类重写此方法响应状态变化 ``` **帧更新链**: ``` StateMachine._process(delta) └── current_state.on_process(delta) └── CharacterState.updated.emit(delta) StateMachine._physics_process(delta) ├── current_state.on_physics_process(delta) │ └── CharacterState.physics_updated.emit(delta) └── _check_transition() └── _root_node.call(condition) → change_state(uid) ``` **玩家控制器链**: ``` PlayController._physics_process(delta) ├── 读取输入 → _process_direction(raw_input) ├── velocity.x = new_dir.x * speed * strength ├── 方向改变? → direction_changed.emit(dir, strength) ├── 有输入? → moving.emit(dir, strength) └── move_and_slide() ``` ### 如何在代码中连接信号 **PlayerCharacter 已内置连接**: ```gdscript # player_character.gd func _ready(): super() state_machine = get_node_or_null("StateMachine") if state_machine: # 连接状态切换信号 state_machine.state_changed.connect(_on_state_changed) # 子类重写此方法响应状态变化 func _on_state_changed(_old: String, _new: String): pass ``` **外部连接示例**(在场景根脚本 `_ready` 中): ```gdscript # main.gd func _ready(): var player = $PlayerCharacter var sm = $PlayerCharacter/StateMachine # 监听玩家移动 player.moving.connect(_on_player_moving) player.direction_changed.connect(_on_direction_changed) # 监听状态机切换 sm.state_changed.connect(_on_state_changed) # 监听 Idle 状态的进入/退出(用于动画) var idle = $PlayerCharacter/StateMachine/Idle idle.entered.connect(_on_idle_enter) idle.exited.connect(_on_idle_exit) func _on_player_moving(dir: int, strength: float): pass # 更新动画混合树 func _on_direction_changed(dir: int, strength: float): pass # 切换朝向精灵 func _on_state_changed(old: String, new: String): print("状态切换: %s → %s" % [old, new]) func _on_idle_enter(): $AnimationPlayer.play("idle") func _on_idle_exit(): pass # 清理 ``` ## 交互说明 | 操作 | 效果 | |------|------| | 左键拖输出槽(绿)→输入槽(黄) | 创建连线 | | 左键拖节点本体 | 移动节点 | | 左键拖空白区域 | 平移画布 | | 双击连线 | 编辑条件方法名和执行顺序 | | 右键连线 | 删除连线 | | 刷新按钮 | 重新加载当前状态配置 | | 保存按钮 | 将画布数据写回 CharacterState | ## PlayerController 配置 | 属性 | 说明 | |------|------| | 当前移动模式 | 左右移动 / 四方向 / 八方向 | | 左移/右移/上移/下移动作 | InputMap 中已注册的输入动作名 | | 当前朝向 | 默认朝向(下拉选择 8 方向) | | 玩家数据 | PlayerData 资源引用 | | 重力值 | 默认取 `physics/2d/default_gravity` | ## 依赖 - Godot 4.3+ - 无外部依赖,纯 GDScript 实现 ## 版本 1.0 — 初始版本