〇两种形态:先选对(30 秒)
同一个模块里给了两种状态机 —— 不是"新旧",是两种用法。
| 轻量(Lightweight) | 重量级(Heavyweight) | |
|---|---|---|
| 用来管 | 游戏流程、界面栈、玩法阶段 | 复杂 AI:Boss、小怪、技能阶段 |
| 状态是什么 | 实现 RevILightState 的类(一般继承 RevLightStateBase) | 继承 RevHeavyFsmState<状态枚举, 宿主接口> 的类 |
| 类型安全 | 泛型 RevLightStateMachine<T>(T 是基类/接口) | 状态用枚举索引:RevHeavyFsm<BossState, IBossObj> |
| 特色能力 | 可异步切换、返回栈版本(Push/Pop) | 切换历史、异步进出(PrepareEnterAsync)、FixedUpdate/LateUpdate |
| 典型代码量 | 一个状态类 10 行 | 一个状态类 + 一个行为接口 |
一句话选法
流程/界面 → 轻量;敌人/角色行为 → 重量级。
拿不准就用轻量(它更简单,也能覆盖大部分玩法阶段)。
一轻量状态机:3 分钟写游戏流程
目标:登录 → 大厅 → 战斗,三个状态互相切换。
// ① 状态:继承 RevLightStateBase,重写关心的回调(三个都是可选的) public class LoginState : RevLightStateBase { public override void OnEnter() { /* 打开登录界面、连服务器 */ } public override void OnUpdate(float dt) { /* 需要每帧做的事 */ } public override void OnExit() { /* 关界面、清理 */ } } public class LobbyState : RevLightStateBase { } public class BattleState : RevLightStateBase { } // ② 建机器 + 注册状态(T 写你要共用的基类/接口) var flow = new RevLightStateMachine<RevLightStateBase>(); flow.Register(new LoginState()); flow.Register(new LobbyState()); flow.Register(new BattleState()); // ③ 切状态(走 OnExit → OnEnter) flow.ChangeTo(flow.CurrentState is LoginState ? GetLobby() : GetBattle()); // ④ 每帧驱动(谁来调见第五节) flow.Update(Time.deltaTime); // ⑤ 想监听切换(埋点、日志、音效) flow.StateChanged += (from, to) => RevLog.Info($"流程:{from?.GetType().Name} → {to?.GetType().Name}", "Flow"); // ⑥ 常用查询 bool inBattle = flow.Is<BattleState>(); float t = flow.StateTime; // 当前状态已经持续了多久(做超时很有用)
异步切换(要等加载完才进下一个状态)
await flow.ChangeToAsync(GetBattle()) —— 切换过程中 IsTransitioning 为 true、PendingState 是目标状态;
想取消就 flow.CancelAsyncTransition()。
二重量级状态机:3 分钟给 Boss 写状态
关键设计:状态类只面向"行为接口"编程,不认识具体 MonoBehaviour —— 所以状态逻辑能单测。
// ① 定义宿主的行为接口:Boss 有哪些能力,由业务说(继承 RevIHeavyFsmOwner 标记接口) public interface IBossObj : RevIHeavyFsmOwner { float Health { get; } void MoveTo(Vector3 pos); void PlayAnim(string name); } // ② 状态枚举:Boss 有哪些状态 public enum BossState { Idle, Chase, Attack, Die } // ③ 状态类:泛型参数 = (状态枚举, 宿主接口) public class BossChaseState : RevHeavyFsmState<BossState, IBossObj> { public override void OnEnter() { Owner.PlayAnim("run"); // Owner 就是宿主(IBossObj) } public override void OnUpdate(float dt) { Owner.MoveTo(PlayerPos()); if (NearPlayer()) Fsm.ChangeState(BossState.Attack, "进入攻击距离"); } public override void OnExit() { Owner.PlayAnim("idle"); } } // ④ 组装 + 驱动(宿主类里:Boss 自己实现 IBossObj) _fsm = new RevHeavyFsm<BossState, IBossObj>(this); _fsm.AddState(BossState.Idle, new BossIdleState()); _fsm.AddState(BossState.Chase, new BossChaseState()); _fsm.AddState(BossState.Attack, new BossAttackState()); _fsm.ChangeState(BossState.Idle, "初始化"); void Update() { _fsm.UpdateState(Time.deltaTime); } // 每帧驱动 void FixedUpdate() { _fsm.FixedUpdateState(Time.fixedDeltaTime); } void OnDestroy() { _fsm.Dispose(); }
异步进出(播放动画、等技能前摇)
重写
PrepareEnterAsync(token) / PrepareExitAsync(token) 返回一个 RevTask,
用 ChangeStateAsync(...) 切换时框架会等你完成。
为什么值得这么绕一层接口
状态类里不出现
BossController、不出现 GetComponent ——
于是"Boss 在血量低于 30% 时该进狂暴"这种规则,可以用一个假的宿主实现来跑单元测试。
三对照表(两套 API 全列)
轻量:RevLightStateMachine<T>
| 我想… | 这么写 |
|---|---|
| 注册状态 | machine.Register(new LoginState()) |
| 切换(同步 / 异步) | machine.ChangeTo(state) · await machine.ChangeToAsync(state) |
| 每帧驱动 | machine.Update(deltaTime) |
| 查询当前 / 判断类型 / 已持续多久 | machine.CurrentState · machine.Is<BattleState>() · machine.StateTime |
| 切换中想知道目标 | machine.IsTransitioning · machine.PendingState · CancelAsyncTransition() |
| 监听切换 | machine.StateChanged += (from, to) => { } |
| 状态回调 | 继承 RevLightStateBase:OnEnter / OnExit / OnUpdate / OnSuspend / OnResume |
重量级:RevHeavyFsm<TStateEnum, TOwner>
| 我想… | 这么写 |
|---|---|
| 建机器(宿主传自己) | new RevHeavyFsm<BossState, IBossObj>(this) |
| 增 / 删 / 查状态 | fsm.AddState(BossState.Idle, new BossIdleState()) · RemoveState · HasState |
| 切换 | fsm.ChangeState(BossState.Attack, "进入攻击距离")(返回 bool:被拒绝时是 false) |
| 异步切换 | await fsm.ChangeStateAsync(...) · 取消:fsm.CancelAsyncTransition() |
| 驱动三种帧 | fsm.UpdateState(dt) · FixedUpdateState · LateUpdateState |
| 看重切历史 / 重置 / 清理 | fsm.GetHistory() · fsm.Reset(BossState.Idle) · fsm.Clear() · fsm.Dispose() |
| 状态回调 | 继承 RevHeavyFsmState<TEnum, TOwner>:OnEnter/OnExit/OnUpdate/OnFixedUpdate/OnLateUpdate/PrepareEnterAsync/PrepareExitAsync |
状态里怎么拿到"宿主"和"状态机"
基类已经给了:
Owner(你的行为接口实例)与 Fsm(当前状态机)。
不要再自己维护一份 static 引用 —— 那是早期实现最容易出错的写法。
四返回栈状态机(UI 栈 / 流程栈)
当"新状态压在旧状态上面,退出后回到上一个"时,用栈版本。
// 界面栈:设置界面压在背包上面,返回时回到背包 var uiStack = new RevLightStackStateMachine<RevLightStateBase>(); uiStack.Push(new BagState()); // 压入(原来那个被挂起:OnSuspend) uiStack.Push(new SettingsState()); // 再压一个 var top = uiStack.Pop(); // 弹出 → 下面那个自动 OnResume 回来 var under = uiStack.Under; // 只在"下面那个"上做点事(不弹它) int n = uiStack.Count; // 栈里几个 uiStack.Change(new ShopState()); // 替换栈顶(Pop + Push 一步) uiStack.Clear(); // 全清(回主界面) uiStack.StatePushed += s => RevLog.Info($"进入 {s.GetType().Name}", "UIStack"); uiStack.StatePopped += s => RevLog.Info($"退出 {s.GetType().Name}", "UIStack");
栈状态用 OnSuspend / OnResume
被压在下面时
OnSuspend()(暂停逻辑、停特效),弹回来时 OnResume()。
这与 UI 系统的 OnCovered(bool) 是同一个思路(界面被盖住/恢复)。
五谁负责每帧驱动(最常问)
状态机是纯逻辑:它不自己挂物体、不自己抢 Update —— 由你决定在哪驱动,这样它才能被单测。
| 你的宿主是什么 | 推荐驱动方式 |
|---|---|
| MonoBehaviour(角色、Boss) | 在它自己的 Update 里:_fsm.UpdateState(Time.deltaTime) |
| 纯 C# 类(流程管理器、玩法模块) | 用公共 Mono 模块注册:RevMono.AddUpdate(dt => flow.Update(dt), owner: this) |
| 只想"定时看一眼"(如从登录轮询到服务器就绪) | RevTimer.Every(0.2f, () => …)(见《计时器系统》) |
为什么不做成自动驱动
自动挂 Update 会带来两个问题:① 纯 C# 里跑不了断言;② 谁在驱动不明确(出了问题时找不到"到底谁在推它")。
现在这样:你看得见驱动点,测试里也能手动喂时间。
六新手最容易踩的 5 个坑
| 坑 | 正确做法 |
|---|---|
| ① 往状态对象里塞"这一局的临时数据" | 状态对象是复用的;临时数据放宿主(Owner)或上下文里 |
② 忘了每帧调用 Update/UpdateState | 驱动点只能有一处(第五节);重复驱动会让状态走两遍 |
③ 在 OnUpdate 里频繁切状态 | 切状态有开销(OnExit/OnEnter);用"条件 + 冷却/阈值"避免每帧抖动(抖动=状态在两个状态间跳) |
④ 忽略 ChangeState 的返回值 | 返回 false = 这次切换被拒绝(异步切换中/状态不存在)→ 需要时判断并打日志 |
| ⑤ 异步切换时又发起一次切换 | 用 IsTransitioning/PendingState 判重,或先 CancelAsyncTransition() |
最贵的一课:状态抖动
"进入攻击距离 → 攻击;一离开就回追击;于是每帧来回切" —— 这类 bug 症状是
动画抽搐、日志刷屏、性能掉。解决:阈值要带滞回(进 3 米、退 4 米),或加最短持续时间(
StateTime 就是给你判这个的)。
七相关文档
- 《状态机 · 架构解析》 —— 设计论证:两种形态的分工、泛型宿主接口为什么这么设计、异步切换的实现
- 《状态机 Demo 示例讲解》 —— Boss AI / 游戏流程 / UI 栈三个完整例子
- 《公共 Mono 模块 · 使用说明》 —— 纯 C# 流程管理器怎么拿每帧回调
- 文档总入口 · 在线文档站