Godot 脚本状态机模式:角色、UI 和流程控制如何避免 if 地狱

讨论 Godot 中脚本状态机的组织方式、状态对象、进入退出、转移条件、调试和复用边界。

状态一多,if 就会变成迷宫

Godot 脚本写起来很快,if is_attackingif is_jumpingif menu_open 很自然。功能继续加,角色会有待机、移动、跳跃、攻击、受击、死亡、攀爬、游泳;UI 会有加载、展示、提交中、失败、成功;流程会有登录、选服、进大厅、重连。条件越来越多,脚本会变成 if 地狱。

状态机不是银弹,但它能让“当前处于什么状态”“能转到哪里”“进入和退出做什么”变得清楚。Godot 里可以用节点状态、脚本对象状态、枚举状态,选择取决于复杂度。

flowchart TD
    A[输入/事件] --> B[StateMachine]
    B --> C[当前 State]
    C --> D{是否允许转移?}
    D -->|否| E[忽略/缓冲]
    D -->|是| F[exit 当前状态]
    F --> G[enter 新状态]
    G --> H[update/physics_update]
    H --> C
    B --> I[调试当前状态和历史]

简单状态用枚举,复杂状态用对象

不是所有地方都需要完整状态机类。一个按钮提交状态,枚举 Idle/Pending/Done/Error 就够。角色控制、Boss AI、登录流程这种复杂对象,则适合状态对象:每个状态有 enter、exit、update、handle_event。

状态对象可以是脚本 Resource、RefCounted,或者 Node 子节点。Node 状态方便在编辑器里组织和调试,但实例多时成本稍高;脚本对象轻量,适合纯逻辑。项目可以两种都用,但要有规范。

无论形式如何,状态机都应集中管理当前状态。不要状态对象自己随意改全局变量,也不要外部脚本直接设置一堆布尔。转移通过 change_state(),方便记录和校验。

enter 和 exit 要成对

状态切换最容易漏清理。进入攻击状态播放动画、打开 hitbox、锁输入;退出时必须关闭 hitbox、解锁或交给下一状态。若攻击被受击打断,exit 仍然要执行。把清理写在动画结束回调里不够,因为不是所有退出都来自动画结束。

状态的 enter 接收上下文,比如攻击技能 ID、受击方向、目标节点。exit 可以知道下一状态,决定是否保留某些效果。比如从攻击转连招可能保留输入缓冲,从攻击转死亡则清全部。

异常路径也要考虑。节点释放、场景切换、暂停时,状态机应有 shutdown 或 force_exit,避免状态残留。

转移条件要可读

状态机的价值在于转移清楚。可以用表或代码声明:Idle 可以到 Move、Attack、Jump;Attack 可以到 Combo、Dodge、Hit、Death;Death 不可转出。转移失败要能返回原因。

不要让每个状态里写一堆跨状态判断,最后还是迷宫。复杂规则可以交给 guard 函数,但转移关系应该能被调试工具展示。

输入缓冲也属于状态机。玩家在攻击收招前按闪避,状态机记录意图,到了可取消窗口再转移。缓冲规则如果散在输入脚本里,会和状态机打架。

UI 流程也适合状态机

状态机不只给角色。登录页有输入账号、请求中、错误、二次验证、成功;领奖弹窗有展示、提交中、成功、失败;下载页有检查、下载、校验、安装、失败。用状态机管理 UI,可以避免按钮在错误状态下可点。

UI 状态机还适合恢复。页面关闭再打开,从模型读取当前状态,进入正确 UI 状态。请求迟到时,状态机判断是否仍然接受结果。

调试状态历史

状态问题很难靠截图。调试面板应显示当前状态、上一状态、最近转移历史、转移原因、被拒绝的事件。角色卡住时,看状态历史能知道是不是卡在 AttackRecovery;登录页不动时,能看到是否在 WaitingToken。

状态机也可以输出 mermaid 或文本图,帮助设计和程序讨论流程。状态越多,越需要可视化。

小结

Godot 脚本状态机的目标,是让复杂流程从布尔组合变成明确状态和转移。简单场景用枚举,复杂流程用状态对象,enter/exit 成对,转移条件集中,UI 流程也纳入状态机,调试记录状态历史。这样项目不会随着功能增加慢慢陷入 if 地狱。
我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

我会给状态机基类加最近 20 次转移记录,并在测试包里显示。很多“偶现卡死”其实只要看到状态历史,就能知道哪条退出路径没清理。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页