返回
玩法拆解 · 代码 · Demo
战斗对抗热门

用状态机组织战斗单位行为

本文拆解战斗单位行为的状态机组织模式,详解将复杂逻辑拆解为独立状态节点的核心机制。读者将掌握如何构建状态转移表以消除冗长条件判断,实现战斗逻辑的可视化调试与低成本扩展,有效解决多行为耦合难题。

19

玩法拆解

为什么需要状态机
  • 复杂行为可读性差
  • 需求变化导致分支膨胀
拆分建议
  • Idle/Move/Attack/Hitstun/Dead
  • 状态间用事件驱动

关键代码

事件驱动(简化)
ts· 13 行
玩法行为:本文属于「战斗对抗」母型围绕对手与技能做交换与博弈;核心是窗口、资源与读招。
查看母型详解

Demo

最小可交互:事件驱动的战斗 FSM(用于演示状态切换与可测试转移)。

用状态机组织战斗单位行为 Demo
游戏加载中…

文章

用状态机组织战斗单位行为

一句话本质

表面上是把单位行为拆成状态和事件;实际上核心机制是用“状态”封装当前行为逻辑,用“事件”驱动状态切换;本文重点解释如何构建最小闭环、收敛上下文依赖以及设计可测试的转移逻辑。

战斗单位的行为一旦开始“加需求”,最容易变成这种形态:

  • 追击时被打断怎么办?
  • 攻击前摇能不能被打断?
  • 受击硬直结束后回到哪里?
  • 仇恨切换、丢失目标、回到巡逻怎么拼?

如果用 if / else 把这些拼在一起,代码会快速变成“谁都不敢改”的分支地狱。

这篇文章给一个可落地、可测试、可扩展的做法:用有限状态机(FSM)组织战斗单位行为,并把“状态切换”统一成事件驱动。

---

1. 核心结论:把行为拆成“状态”,把切换拆成“事件”

推荐把单位的行为分成两层:

  1. 状态(State):单位当前处于哪种行为模式(Idle / Move / Attack / Hitstun / Dead …)
  2. 事件(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 / velocity
  • targetId / distanceToTarget
  • cooldowns
  • hp
  • events(本帧输入/感知事件)

状态函数只读写 context,不直接操作引擎全局。

---

4. 事件来源:输入/感知/动画/伤害,都统一成 Event

很多“分支爆炸”来自于事件来源分散:

  • AI 感知(看到目标、丢失目标)
  • 碰撞与距离(进入攻击范围、离开范围)
  • 动画回调(前摇结束、出手帧、后摇结束)
  • 受击系统(被打、击退、控制)

推荐做法:每帧把这些统一塞进 events[],状态机只消费 events。

这样你甚至可以做“录制/回放”:记录事件序列就能复现行为。

---

5. 迁移策略:从 if-else 到 FSM 不需要一次性重写

一个安全的迁移节奏:

  1. 先把“攻击流程”提炼成 attack 状态(最容易分支爆炸的部分)
  2. 再把“追击/脱战”提炼成 move 与 idle
  3. 最后补 hitstun 与各种控制(眩晕/击退/沉默…)

每一步都能运行、能回归测试,不会把项目搞崩。

---

6. 可测试性:transition 是最值得写单测的地方

最容易写单测、也最能挡住回归 bug 的部分,是 transition(state, event):

  • 给定当前 state + event,是否切到正确 state?
  • guard 条件(cd 未好不能攻击、HP<=0 必死)是否生效?

推荐把 transition 写成纯函数(或接近纯函数),这样测试成本最低。

你可以用“状态转移表”的方式列出关键场景:

  • idle + SEE_TARGET => move
  • move + IN_RANGE => attack
  • attack + 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 配成表,支持不同怪物复用同一套框架

边界条件

  • 需要处理攻击前摇是否允许被打断,以及受击硬直结束后如何判定下一状态。
  • 需要处理仇恨丢失、目标脱离范围等感知事件,确保单位能正确回到巡逻或待机状态。
  • 需要处理 HP 归零等致命条件,确保单位能平滑过渡到死亡状态且不可逆。
  • 需要处理冷却时间未就绪的情况,防止在 guard 条件不满足时强行触发状态切换。
  • 需要处理状态重置是否干净,避免上一状态残留数据污染当前状态逻辑。

设计取舍

  • 收益:行为解耦,状态内部逻辑简单且独立;代价:需要额外维护状态列表和事件队列;替代:用 if/else 分支直接控制逻辑。
  • 收益:状态切换统一由事件驱动,支持录制回放行为;代价:需要设计统一的事件来源和消费接口;替代:在状态内部直接读取全局变量或分散的感知逻辑。
  • 收益:transition 逻辑可写成纯函数,极大降低单元测试成本;代价:需要预先定义完整的状态转移表和 guard 条件;替代:在代码中硬编码复杂的 if/else 判断逻辑。