今日速览
- 一个角色最容易写崩的地方不是物理,而是「一堆
if is_on_floor() and not is_attacking and not is_hurt」——状态越多,组合爆炸越快。 - 有限状态机(FSM)的核心只有两件事:当前状态是唯一真相,以及状态切换必须走同一个入口。
- Godot 里最省心的实现是「一个状态一个 Node + 一个脚本」,用父节点
StateMachine统一调度enter / update / physics_update / exit。 - 状态脚本只写「在这个状态下做什么」,不要在里面判断「我是不是该切走了」的十种情况——判断条件放在状态自己的
transitions里,越短越好。 - 有了清晰的状态边界,DeepSeek 每次只需要看一个 30 行的状态脚本,生成的代码准确率会明显提高。
核心学习内容
一、为什么 if-else 会烂掉
假设角色有 Idle / Run / Jump / Fall / Attack / Hurt / Dead 七个状态,用布尔变量管理就是 7 个变量、几十种组合。表现是:受伤时还能攻击、死亡后还在播放跑步动画、跳跃中按攻击导致动画卡住。这不是「手滑写错」,是结构本身不允许你去验证所有组合。
FSM 的解法是把「同时成立的多个布尔」换成「此刻只成立的一个枚举」。任何时刻只可能有一个状态,于是你需要验证的分支从 2^7 降到 7。
二、最小可用实现
父节点 StateMachine(继承 Node),子节点是各个状态。状态脚本统一继承一个基类:
class_name State
extends Node
var machine: StateMachine
var host: CharacterBody2D
func enter(_msg := {}) -> void: pass
func exit() -> void: pass
func update(_delta: float) -> void: pass
func physics_update(_delta: float) -> void: pass
StateMachine 负责持有 current_state,并暴露唯一入口:
class_name StateMachine
extends Node
@export var initial_state: State
var current_state: State
var host: CharacterBody2D
func _ready() -> void:
host = owner
for child in get_children():
if child is State:
child.machine = self
child.host = host
transition_to(initial_state.name.to_lower())
func transition_to(state_name: String, msg := {}) -> void:
if current_state:
current_state.exit()
current_state = get_node(state_name)
current_state.enter(msg)
func _physics_process(delta: float) -> void:
if current_state:
current_state.physics_update(delta)
if host:
host.move_and_slide()
注意两个细节:move_and_slide() 放在状态机里统一调用,而不是每个状态各调一次——避免同一帧被调用两次导致速度翻倍;enter(msg) 允许带参数,比如 transition_to("hurt", {"damage": 12, "from": global_position.x}),这就是状态间的「消息传递」。
三、让状态保持「瘦」
一个健康的 AttackState 大概长这样,职责只有三件:播动画、等动画结束、切回去。
extends State
func enter(_msg := {}) -> void:
host.velocity.x = 0
host.animation.play("attack")
func physics_update(_delta: float) -> void:
if host.animation.is_playing():
return
machine.transition_to("idle")
而「能不能攻击」「要不要掉血」这类外部条件应该由 host 或 hurtbox 主动发信号,状态机收到后再切换。这样状态之间互不认识,加一个新状态(比如 Dash)不需要改任何一个老状态。
四、FSM 与行为树怎么选
- 单个角色、状态数量 < 10、状态间跳转规则明确 → FSM 足够,调试时打印
current_state.name就能定位问题。 - 敌人需要「按优先级选行为」「多步组合任务」→ 上行为树(之前第 45 天那篇)。
- 两者可以共存:行为树负责选目标,FSM 负责执行具体动作。
🎯 DeepSeek 提示词
提示词 1:把一个巨型 if-else 角色脚本重构成 FSM
你是一位 Godot 4.x 的 GDScript 重构专家。
【任务】把我提供的角色脚本重构成有限状态机架构。
【输入】我会粘贴一个 CharacterBody2D 脚本,里面有大量 is_xxx 布尔变量和嵌套 if-else。
【输出格式】按文件分块输出,每个文件用 ```gdscript 包裹,文件路径写在代码块前一行注释:
1. state_machine.gd(含 transition_to、统一调用 move_and_slide)
2. state.gd(基类)
3. 每个状态一个文件,命名如 idle_state.gd / run_state.gd
4. 修改后的角色主脚本(只保留属性、动画引用、信号连接)
【约束】
- Godot 4.x 语法,使用 class_name,静态类型标注
- 状态脚本内禁止出现 host.is_xxx 形式的布尔判断
- 状态切换只能通过 machine.transition_to(name, msg)
- 保留原有全部行为,不允许"简化掉"任何分支,若有歧义请列出问题而不是猜
【附加】最后给一张"旧变量 → 新状态"的映射表,方便我逐条核对。
用法:把老脚本整段贴进来。重点看它给的映射表,确认没有漏行为再替换文件。
提示词 2:新增一个状态,但不许动老代码
你是一位 Godot 4.x 程序员,现在要往已有的 FSM 里新增状态。
【已有架构】StateMachine 节点 + State 基类(enter/exit/update/physics_update)
+ 若干状态脚本,全部通过 machine.transition_to() 切换。
【当前状态列表】idle, run, jump, fall
【新增状态】dash(冲刺)
【需求】按下 dash 动作时触发;持续 0.18 秒;速度 480 px/s;期间无重力;结束回到 idle 或 fall(看是否在空中);冷却 0.5 秒;有短暂无敌。
【输出】
1. dash_state.gd 完整代码
2. 需要改动的最小 diff(只贴改动行,标注文件)
3. 冷却与无敌用什么方式实现(状态内计时 vs 角色属性),说明理由
【约束】
- 不得修改 idle/run/jump/fall 的现有逻辑
- 用 delta 累加计时,不硬编码帧数
- 若需要新增导出变量,给出 @export 声明
用法:这是 FSM 最爽的地方——新功能是「加文件」而不是「改老文件」,DeepSeek 的出错过不了夜。
提示词 3:当 FSM 出现「卡在某个状态」时的排查
你是 Godot 4.x 调试专家。我的角色卡在 X 状态不动了。
【现象】{描述:哪个状态、什么操作后、动画/位移是否还在动}
【我的状态机实现】StateMachine 持有 current_state,状态脚本有 enter/exit/physics_update。
【请按顺序回答】
1. 列出该状态下所有可能导致"永远不切换"的原因(按概率排序,至少 5 条)
2. 每条给一个 30 秒内可验证的检查方法(打印什么变量、在哪打断点)
3. 给出最可能原因的修复代码
4. 顺便指出我的 enter/exit 是否漏了复位(速度、动画、计时器、信号连接)
【约束】不要重写整个状态机,只针对问题给最小修改。
用法:卡状态是 FSM 最常见的 bug,90% 是 exit() 没复位某个变量,或者切换条件依赖了一个永远不会再为真的信号。
学习心得
做一款能上架 Steam 的游戏,真正拖慢进度的不是「不会写」,而是代码改到第三个月就不敢动了。FSM 是性价比最高的一次结构性投资:它把「加一个冲刺、加一个受击、加一个处决」从「重读 600 行脚本」变成「新建 40 行文件」。对用 DeepSeek 做游戏的我们来说,这一点更关键——AI 最擅长在边界清晰的小文件里写出正确代码,最不擅长在纠缠两千行的上帝脚本里改对一行。今天就把你项目里最乱的那个角色脚本换掉。
明日关注
- 把
hurt和dead做成两个状态,验证「受伤无敌帧」不再和攻击状态打架 - 给 StateMachine 加一个
@export var debug_print: bool,切换时打印旧 → 新,方便录屏复盘 - 思考敌人 AI:先用 FSM 写「巡逻 / 追击 / 攻击」,再评估是否需要升级成行为树
本文由自动化学习流水线每日生成,坚持即学即记。
评论