今日速览
- 角色行为写崩的根源,往往不是逻辑难,而是一堆布尔标志位互相打架(
isJumping_和isDucking_同时为真怎么办?)。 - 有限状态机(FSM,Finite State Machine)只做三件事:一组固定状态、同一时刻只能处于一个状态、输入/事件触发状态之间跳转。
- FSM 的实现有一条「升级阶梯」:枚举 + switch(match)→ 状态模式(每个状态一个类)→ 分层状态机 / 下推自动机。绝大多数独立游戏,前两级就够了。
- 关键收益是「把某个状态专属的代码和数据收拢到一处」:蓄力计时器只该活在「蹲下」状态里,而不是挂在角色身上。
- Godot 官方 demo 项目
2d/finite_state_machine演示了 Node/场景版实现:每个状态是一个子节点,Transition 在编辑器里可见,适合逻辑很重的敌人和 Boss。
核心学习内容
1. 为什么标志位(flag soup)一定会崩
《Game Programming Patterns》用一个主角英雄举例:一开始「按 B 起跳」只要一行代码。马上发现问题——空中可以无限跳。于是加一个 isJumping_。接着要「按下蹲下」,再加 isDucking_。然后要「空中下砸」,又发现下砸时没拦住空中跳……每加一个动作,就要回头检查并修补所有已有的标志位组合。
问题不在于代码难写,而在于这些布尔量之间存在大量非法组合:isJumping_ 与 isDucking_ 不该同时为真。当一个变量只能取有限几种值、且任意时刻只有一种成立时,它本质上是「状态」,而你却用一堆互相没有约束的开关去拼——组合数是爆炸的(2ⁿ),bug 是必然的。
经验信号:当你在某段代码里反复做「如果 A 且 不是 B 且 不是 C,则……」时,你需要的不是再多一个标志位,而是一个显式的状态机。
2. 第一级:枚举 + match(入门首选)
最朴素的实现,就是把「状态」变成一个枚举,把「当前是什么状态」变成一个字段,然后先按状态分支、再按输入分支:
enum State { IDLE, RUN, JUMP, FALL, ATTACK }
var state: State = State.IDLE
func _physics_process(delta: float) -> void:
match state:
State.IDLE:
_state_idle(delta)
State.RUN:
_state_run(delta)
# ...
两个要点:
- 先切状态、再切输入。反过来(先切输入、再切状态)会把「同一个状态的逻辑」打散到各处,改一个动作要满文件找。
- 可变状态只剩一个字段。判断「能不能跳」变成
if state == State.IDLE or state == State.RUN:,非法组合从语言层面就不存在了。
代价是:当某个状态需要自己专属的数据(比如「蹲下蓄力」要计时),这个数据会挂在角色身上,而它只在「蹲下」时有意义——不够干净,但小项目完全够用。
3. 第二级:状态模式(State Pattern)
当某个状态的逻辑多到不想再往角色类里塞东西时,就升级成 State 模式:定义一个状态接口(enter / exit / handle_input / update),每个状态写成一个类,角色只持有一个「当前状态对象」,所有调用都委托给它。
原来的「蹲下蓄力」计时器,现在顺理成章地变成 DuckingState 的私有成员——专属数据跟着专属状态走,对象模型终于和现实对齐了。切换状态就是把 current_state 指向另一个对象。
两种状态对象的管理方式:
- 无状态的状态用静态单例(比如「站立」不需要存任何东西,一份实例全局复用,零分配);
- 有状态的状态(带计时器、计数器)就每次进入时 new 一个,退出时丢弃。
4. 在 Godot 4 里怎么落地
- 角色(玩家):逻辑不复杂,用枚举 +
match最省事,一个.gd文件搞定,配合_physics_process里的物理与碰撞。 - 敌人 / Boss:状态多、每个状态有 enter/exit 与大量行为,推荐 Node 版状态机——官方 demo 做法是:
StateMachine节点下放一堆状态子节点(Idle、Chase、Attack…),每个状态节点实现enter()、exit()、update(delta)、physics_update(delta);状态机在_process/_physics_process里转发给当前状态,并暴露transition_to("Chase")。好处是状态在场景树/编辑器里可见,非程序员也能看懂结构。 - 无论哪种,进入状态时重置计数器、退出状态时清理临时变量(在
enter/exit里做),能省掉一大类「状态残留」bug。
🎯 DeepSeek 提示词
以下 3 条覆盖「生成 → 重构 → 敌人状态机」,可直接复制修改使用。
提示词 1:生成 Godot 4 玩家状态机(枚举版)
你是 Godot 4.3 的 GDScript 专家,熟悉 CharacterBody2D 与状态机模式。
【项目背景】2D 平台动作游戏,玩家控制一个 CharacterBody2D,需要:待机、跑、跳、下落、攻击 五种行为。
【任务】写一个内嵌枚举的玩家状态机脚本。
【要求】
1. 用 enum State { IDLE, RUN, JUMP, FALL, ATTACK } 和 var state: State,先用 match(state) 分发,再处理输入。
2. 在 _physics_process(delta) 中统一处理重力、move_and_slide();地面检测用 is_on_floor()。
3. 每个状态一个私有方法 _state_idle/_state_run/_state_jump/_state_fall/_state_attack;进入某状态要做的事(如重置计时器)集中写。
4. 攻击有 0.3 秒冷却和前摇,冷却期间不能重复触发;跳跃要求在地面或 coyote time(0.1 秒)内。
5. 用 @export 暴露 move_speed、jump_velocity、gravity 等参数,方便在检查器里调。
6. 全中文注释;只输出完整的 .gd 文件;不要用 Godot 3 的旧语法。
【输出格式】先给 1 行状态转移表(文本),再给完整代码。
【约束】不引入第三方插件;不修改 project.godot;代码需能直接挂在 CharacterBody2D 上运行。
提示词 2:把「标志位堆叠」的老代码重构成状态机
你是一位代码重构专家。我有一段用多个布尔标志位控制角色行为的代码,越改 bug 越多。
【输入代码】
{在这里粘贴你的旧代码,例如用了 isJumping_ / isDucking_ / isAttacking_ 等一堆标志位}
【任务】把它重构成有限状态机。
【输出格式(Markdown)】
1. 诊断:列出旧代码里哪些标志位组合是「非法/矛盾」的,逐条说明会导致什么 bug。
2. 状态清单:为每个状态命名,并写一句它的职责。
3. 状态转移表:Markdown 表格,列 = 当前状态 | 触发输入/事件 | 目标状态 | 副作用。
4. 重构后代码:完整可运行版本,保持原语言与引擎(若未说明,默认 Godot 4 GDScript)。
5. 行为对照:说明重构后哪些旧 bug 被「从结构上」消除,而不是靠 if 补丁。
【约束】不改变原有对外接口(方法名/信号名保持不变),除非在「迁移说明」里明确指出并给出替换方案;语言简体中文。
提示词 3:生成 Node 版敌人状态机(巡逻/追击/攻击/受击/死亡)
你是 Godot 4.3 GDScript 专家,请用「每个状态一个节点」的方式设计敌人 AI。
【任务】为一种近战敌人实现 Node 版有限状态机,状态:Patrol(巡逻)、Chase(追击)、Attack(攻击)、Hurt(受击)、Dead(死亡)。
【架构要求】
1. 一个 StateMachine 节点,挂一个 `current_state`,暴露 transition_to(name: String) 与 update/physics_update 转发;子节点是各个状态。
2. 每个状态脚本继承一个 State 基类,实现 enter()、exit()、update(delta)、physics_update(delta)。
3. Chase 用玩家的全局坐标朝目标移动(提示:可用 move_and_slide);进入 Attack 前做一次「攻击冷却 + 前摇」,命中判定用 Area2D 或距离判断。
4. Hurt:受击后 0.2 秒硬直再回到 Chase;HP<=0 时 transition_to("Dead"),Dead 状态做一次淡出并 queue_free。
5. Patrol 在两点之间往返,到达边缘或障碍时转身。
【输出】
1. 场景树结构说明(缩进文本树,标注每个节点类型)。
2. 每个 .gd 文件的完整代码。
3. 一份「状态转移表」Markdown 表格 + 3 步挂载说明。
【约束】Godot 4.3 语法,全中文注释;不依赖第三方插件;若某个状态不需要比基类更多的数据,直接用单例复用,有数据的才实例化。
学习心得
这一课补上了「用 DeepSeek 做一款 Steam 游戏」里最容易被忽略、却最影响能不能长期维护的一环:如何组织会变化的行为。对我们的《拾光·迷你肉鸽》来说,玩家移动、敌人 AI、Boss 分阶段,本质都是状态机——分层数、战斗阶段、卡牌结算时机都能用状态来建模。更强的意义在于:状态转移表是纯文本,这正是 DeepSeek 最擅长产出的东西。我们可以先让 AI 输出「状态清单 + 转移表」,人工审一遍逻辑,再让它生成代码;重构时也只需把旧代码喂给提示词 2,让 AI 顺手列出被消除的历史 bug。这样既借助 AI 提速,又把「逻辑对不对」这件事留给人来判断——分工刚刚好。
明日关注
- 用提示词 1 给自己的角色落地第一版状态机,在 Godot 里跑通 idle→run→jump→fall 的循环,确认没有「空中乱跳/状态卡死」。
- 挑一段自己写的标志位代码,用提示词 2 重构,重点看它列出的「非法组合」是否说中了你踩过的坑。
- 用提示词 3 生成一个敌人 FSM,并补一张手绘状态转移图存档到
docs/fsm_enemy.md,作为后续 Boss 分阶段设计的基础。
本文由自动化学习流水线每日生成,坚持即学即记。
评论