战斗状态机热门
用状态机组织战斗单位行为
把单位行为拆成可测试、可组合的状态,减少 if-else 爆炸。
0
玩法拆解
为什么需要状态机
- 复杂行为可读性差
- 需求变化导致分支膨胀
拆分建议
- Idle/Move/Attack/Hitstun/Dead
- 状态间用事件驱动
关键代码
事件驱动(简化)
ts· 13 行
Demo
最小可交互:事件驱动的战斗 FSM(用于演示状态切换与可测试转移)。
文章
文章以 Markdown/MDX 文本子集渲染(不支持自定义组件)。
用状态机组织战斗单位行为
战斗单位的行为一旦开始“加需求”,最容易变成这种形态:
- 追击时被打断怎么办?
- 攻击前摇能不能被打断?
- 受击硬直结束后回到哪里?
- 仇恨切换、丢失目标、回到巡逻怎么拼?
如果用 if / else 把这些拼在一起,代码会快速变成“谁都不敢改”的分支地狱。
这篇文章给一个可落地、可测试、可扩展的做法:用有限状态机(FSM)组织战斗单位行为,并把“状态切换”统一成事件驱动。
---
1. 核心结论:把行为拆成“状态”,把切换拆成“事件”
推荐把单位的行为分成两层:
- 状态(State):单位当前处于哪种行为模式(Idle / Move / Attack / Hitstun / Dead …)
- 事件(Event):触发状态变化的事实(SEE_TARGET / IN_RANGE / TOOK_HIT / ANIM_DONE / LOST_TARGET …)
你要追求的是:
- 状态内部逻辑尽量简单(只处理“在这个状态里要做什么”)
- 状态切换只发生在一个统一的地方(transition/guard)
---
2. 最小状态集:先把“可玩闭环”跑通
对多数战斗单位(近战 AI),“最小可玩闭环”一般是:
idle:站立/巡逻move:朝目标移动attack:攻击(包含前摇/出手/后摇)hitstun:受击硬直(可打断)dead:死亡(不可逆)
其中最关键的是:attack 里要明确“能否被打断”,否则后续再补打断会很痛苦。
---
3. 状态机的接口:把外界依赖收敛到 Context
不要让状态里随便访问全局对象或到处 import,否则测试会很难写。
推荐每个单位有一个 context(或称 blackboard),提供状态需要的最小信息:
pos/velocitytargetId/distanceToTargetcooldownshpevents(本帧输入/感知事件)
状态函数只读写 context,不直接操作引擎全局。
---
4. 事件来源:输入/感知/动画/伤害,都统一成 Event
很多“分支爆炸”来自于事件来源分散:
- AI 感知(看到目标、丢失目标)
- 碰撞与距离(进入攻击范围、离开范围)
- 动画回调(前摇结束、出手帧、后摇结束)
- 受击系统(被打、击退、控制)
推荐做法:每帧把这些统一塞进 events[],状态机只消费 events。
这样你甚至可以做“录制/回放”:记录事件序列就能复现行为。
---
5. 迁移策略:从 if-else 到 FSM 不需要一次性重写
一个安全的迁移节奏:
- 先把“攻击流程”提炼成
attack状态(最容易分支爆炸的部分) - 再把“追击/脱战”提炼成
move与idle - 最后补
hitstun与各种控制(眩晕/击退/沉默…)
每一步都能运行、能回归测试,不会把项目搞崩。
---
6. 可测试性:transition 是最值得写单测的地方
最容易写单测、也最能挡住回归 bug 的部分,是 transition(state, event):
- 给定当前 state + event,是否切到正确 state?
- guard 条件(cd 未好不能攻击、HP<=0 必死)是否生效?
推荐把 transition 写成纯函数(或接近纯函数),这样测试成本最低。
你可以用“状态转移表”的方式列出关键场景:
idle + SEE_TARGET => movemove + IN_RANGE => attackattack + TOOK_HIT => hitstun(如果允许打断)hitstun + STUN_END => move/idle(看是否仍有目标)- 任意状态 + HP_ZERO => dead
---
7. 调试与可视化:先把“状态名”打出来就能救命
MVP 阶段不需要高级工具,先做到:
- 屏幕上显示单位当前
state - 显示最近 3 个事件(lastEvents)
- 显示目标与距离(targetId / dist)
这会让你在调 AI 行为时节省大量时间。
下一步才是:
- 状态图可视化
- 事件时间线
- 断点式暂停/单步推进
---
8. 下一步扩展(可选)
- 分层状态机(Hierarchical FSM):把
attack拆成windup/strike/recover - 行为树 + 状态机结合:高层决策用 BT,低层动作用 FSM
- 数据驱动:把状态/事件/guard 配成表,支持不同怪物复用同一套框架