动作游戏中,角色在任意时刻可能处于站立、跳跃、攻击、受击等多种状态。状态之间存在复杂的转换和冲突——这是动作游戏开发中最经典的架构问题之一。
在开发一款轻度动作 RPG 时,我经历了三种方案的演进,最终落地于分层状态机。本文记录这个思考过程。
方案一
状态栈
状态栈
➜
方案二
冲突管理器
冲突管理器
➜
方案三 ✓
分层状态机
分层状态机
方案演进路线
状态的基本结构
每个状态拥有独立的生命周期:
OnEnter
进入状态
初始化数据
初始化数据
➜
OnUpdate
每帧执行
驱动逻辑
驱动逻辑
➜
OnExit
离开状态
清理资源
清理资源
状态生命周期三阶段
从持续性角度,状态分为两类:
| 类型 | 行为 | 典型例子 |
|---|---|---|
| 持久状态 | 条件满足就持续存在,每帧检测 | Stand(在地面→生效)、Run(有速度→生效) |
| 触发状态 | 事件触发,执行完毕自动退出 | Attack(动画播完→退出)、Skill(释放完→退出) |
角色通过一张状态表展示当前所有生效的状态。核心问题随之而来——当多个状态同时存在时,冲突怎么办?
==================== 方案一 ====================方案一:状态栈
最直观的思路:每个按钮对应一个状态栈,栈内管理连招序列,所有按钮的状态栈并行执行。
攻击按钮
Attack_3 ← 执行中
Attack_2
Attack_1
跳跃按钮
Jump ← 执行中
闪避按钮
Dodge ← 执行中
每个按钮维护一个状态栈,栈顶为当前执行状态
对于状态混合(如角色在空中),有两种处理方式:
并行方式
Jump
Fall
同时存在,各自执行
VS
串联方式
Jump
➜
Fall
到达最高点后自动切换
⚠️ 问题:耦合度高
每个状态必须知道"下一个是谁"。简单场景可以用配置表,但复杂场景下——
Jump(起跳中)
被击飞
KnockUp
被倒地
KnockDown
正常落下
Fall
每个状态都要维护这样的分支,复杂度 O(n²)
方案二:第三方冲突管理器
既然分散管理太混乱,那就集中——引入一个管理器统一仲裁。
状态 A"后继可以是 B,C,D"
➜
冲突管理器查表裁决
➜
进入状态 B
状态只声明候选后继,管理器负责裁决
⚠️ 问题:冲突关系没有消失
代码层面解耦了,但冲突表的规模仍随状态数增长而膨胀。从"分散在各状态中"变成"集中在一张大表里"——本质困境未解。
方案三:分层状态机(最终方案)
核心思路:按职责分层,层内用状态栈,层间用优先级仲裁。
P3 最高
受击层
Hit / KnockDown / KnockUp
▼ 高优先级直接打断低优先级
P2
技能层
Skill_1 → Skill_2 → Skill_3(层内连招)
▼ 高优先级直接打断低优先级
P1 最低
运动层
Stand / Run / Jump / Fall
分层状态机架构 — 层间优先级自动仲裁,层内独立管理
✓ 为什么这样更好?
- 层内简单 — 技能层只处理技能间的连招和打断,不用关心运动状态
- 跨层自然 — 受击层优先级最高,受击时直接打断技能,无需额外配置
- 复杂度从 O(n²) 降到 O(n) — 每个状态只需关心同层内的少量状态
三种方案对比
| 状态栈 | 冲突管理器 | 分层状态机 | |
|---|---|---|---|
| 冲突管理 | 每个状态自行维护 | 集中在冲突表 | 按层隔离 + 层间优先级 |
| 耦合度 | 高 | 中 | 低 |
| 可维护性 | O(n²) | 表膨胀 | O(n) |
| 连招支持 | 栈式压入,直观 | 需额外设计 | 技能层内栈式管理 |
| 适用场景 | 状态极少的简单游戏 | 中等复杂度 | 动作游戏推荐方案 |
具体实现在 Skill.cs 和 SkillState 中,将普通状态与技能状态统一纳入分层状态机体系管理。相关项目详见 XYGame 项目页。