直接用 enum class + switch 实现游戏 FSM 会导致状态跳转逻辑分散、难以测试和热重载;真实需求需状态携带数据、响应事件、支持嵌套与平滑过渡,应改用面向对象的状态类设计。

为什么别直接用 enum class + switch 写游戏 FSM
因为状态跳转逻辑会迅速散落在各处,比如「角色受击后要进硬直态,但若当前在跳跃中则跳过硬直直接进坠落态」——这种条件嵌套会让 switch 块膨胀、难以测试、更没法热重载状态行为。真实游戏里,状态常需携带私有数据(如跳跃高度、倒计时)、响应不同事件(输入、动画结束、碰撞)、甚至嵌套子状态(如「奔跑中按蹲键→滑铲态」)。纯枚举+分支根本撑不住。
实操建议:
- 把每个状态封装成独立类,继承统一基类
State,实现enter()、update()、exit()和handleEvent() - FSM 本体不存状态值,而持有一个
std::unique_ptr<state></state>指向当前活跃状态 - 状态跳转不靠
switch判定,而是由当前状态的handleEvent()返回下一个状态的工厂函数(如std::function<:unique_ptr>()></:unique_ptr>),或直接调用 FSM 的changeState(std::unique_ptr<state>)</state>
怎么让状态能访问角色共享数据又不耦合
硬编码 Player* 成员会导致状态类和角色强绑定,改角色接口就得全量改状态。正确做法是用「上下文对象」解耦:
定义一个轻量结构体 GameStateContext,只暴露状态真正需要的接口:
立即学习“C++免费学习笔记(深入)”;
struct GameStateContext {
Vec2& position;
float& velocityY;
bool& isGrounded;
void jump();
void playAnimation(const char* name);
EventQueue& eventQueue; // 用于发内部事件,如 "animation_finished"
};所有状态构造时接收 GameStateContext&,内部只调用这些方法。这样状态可复用于不同角色(只要提供对应上下文),单元测试时也容易 mock。
注意点:
- 避免在上下文中塞裸指针或复杂生命周期对象;引用成员必须保证比所有状态活得久
- 不要把输入处理逻辑放进上下文——输入应先由系统收集,再以事件形式(如
InputEvent{Key::A, Pressed})推给当前状态
如何支持状态间的平滑过渡(比如攻击→受伤→倒地)
纯切换会丢失中间帧,角色可能突兀位移或动画撕裂。关键不是“多加一个过渡态”,而是让 FSM 允许并行存在两个状态:
- 主状态(
m_currentState)负责核心逻辑和输入 - 过渡状态(
m_transitionState)只接管动画混合、物理阻尼等瞬时效果,不响应新输入 - 在
update()中,先更新主状态,再按权重插值混合其输出与过渡状态输出(如位置、朝向)
例如受伤过渡:主状态仍是「奔跑」,但过渡状态启动「受击抖动」动画,并在 0.3 秒后自动销毁自己,触发最终进入「倒地」态。这要求 State 基类提供 blendWeight() 和 isTransitionDone() 接口。
性能提示:过渡状态不用每帧 new/delete,可用对象池预分配;权重插值尽量用 SIMD 友好的结构(如把位置/旋转/缩放打包成 Vec4)。
为什么 std::map<:type_index std::unique_ptr>></:type_index> 不适合做状态缓存
想复用已创建的状态实例(比如「空闲态」全局就一个)?用 type_index 当 key 看似合理,但实际踩坑不少:
- 模板实例化导致
std::type_index{typeid(IdleState)}和std::type_index{typeid(IdleState<player>)}</player>不同,泛型状态难管理 - RTTI 在某些嵌入式平台或 -fno-rtti 编译下不可用
- 无法按字符串名查找(不利于编辑器集成或热重载)
更稳的方案是用字符串 ID 显式注册:
class StateFactory {
public:
using Creator = std::function<std::unique_ptr<State>(GameStateContext&)>;
static void registerState(const char* id, Creator creator);
static std::unique_ptr<State> createState(const char* id, GameStateContext& ctx);
};注册时用 "idle"、"jump_start" 这类语义名,运行时通过配置文件或调试命令动态加载,也方便 Unity/Unreal 编辑器插件读取。
真正麻烦的是状态私有数据的序列化——比如「连招态」要记住已输入的按键序列。这部分得每个状态自己实现 serialize()/deserialize(),FSM 层只管转发,别试图统一抽象。



















