7. State

7. State:把输入与行为随状态变化的规则建成可检查状态机,通过可复位因果实验和反例证据验收。

7. State

学习目标

  • 能画出角色状态机
  • 能对比枚举switch与状态类
  • 能解释正交/层次/下推状态机

为什么"7. State"从问题证据开始

游戏角色几乎都有状态:站立、行走、跳跃、攻击。最直觉的实现是枚举加 switch——if (state == JUMPING) ... else if (state == ATTACKING) ...。这段代码今天能跑,但每次加新状态(比如"受伤硬直")都要在每个处理输入/更新的 switch 里加分支;更糟的是,状态之间的转换规则散落在各处,改一处规则要翻遍整个文件。

本页把"无模式基线"定义为"枚举 + switch"的代码,然后引入状态机作为候选机制,验证它是否真的让"加新状态"与"改转换规则"的代价下降。通过条件是新增状态只改一处、转换规则集中可查——而不是 switch 分支越堆越多。

🔮 猜一猜:给状态机加"受伤硬直",枚举方案与状态类方案各改几处?

来源、版本与独立重写边界

本页用作者完整在线正文核对正式标题、设计分叉和时代语境,并以作者源码仓库交叉检查结构。仓库许可证明确正文、HTML与样式为 CC BY-NC-ND 4.0,示例程序等其他文件为 MIT;因此下列中文解释、图示、交互和代码均为独立教学重写,不翻译、拼接或改写受 ND 限制的原文表达。

本章机制与术语

理解 State 需要四个核心概念:

  • :对象当前所处的一种行为模式——同一输入在不同状态下的响应不同。
  • :状态之间的切换,由事件或条件触发——跳跃落地回到站立。
  • :状态 + 转换规则的集合,形式上可用"当前状态 × 事件 → 下一状态"表示。
  • :把每个状态封装成对象,行为与转换规则收进各自类——State 模式的核心机制。

四个概念共同约束"把输入与行为随状态变化的规则建成可检查状态机":任何结论都必须回到"新增状态的成本、转换规则的集中度、状态行为的封装度"三个可观察量。

Design Pattern · Revisited

State — 让每个状态自己决定该做什么

▷ 可交互
同一个角色,四种行为——取决于当前处于哪个状态事件触发转换,每个状态只关心自己负责的事件按空格按空格按空格Idle起始Jumping空中Running移动Attacking出招转换完成枚举 switch:条件膨胀,每个状态的行为挤在一个函数里状态类:每个状态一个类,自己封装行为与转换

第 1 / 5 步 · ① 角色处于 Idle:按空格触发跳跃

状态模式把'当前在哪个状态'变成可替换的对象——加状态不改旧状态代码。

�� 对着动画看:动画是菱形排布的四个状态球——Idle 居中,Jumping/Running/Attacking 环绕,弧线带事件标签(按空格/落地/停止/攻击结束)。注意右侧对比框:枚举 switch 把所有状态的行为挤在一个函数里,状态类 则各自封装。读"枚举与 switch"一节时,想象往动画里加一个"受伤硬直"状态,两种方案各要改几处。

官方结构逐项深读

7. State

State 模式的主张:把"当前处于哪个状态"本身变成一个可替换的对象。角色对象持有一个状态引用,收到输入或每帧更新时,把请求委托给当前状态对象处理——状态对象决定如何响应、以及是否切换到另一状态。加新状态 = 加一个新状态类 + 定义它的转换;改规则 = 改对应状态类。行为随状态变化这件事,被封装进了状态类内部。

We've All Been There

所有游戏程序员都见过这样的代码:一个 handleInput() 函数里,if (state == STANDING) { if (pressed jump) state = JUMPING; } else if (state == JUMPING) { if (pressed down) state = DIVING; }...。单看每行都不难,但状态越多,分支越乱:加一个状态要在每个处理点加分支;改一个转换要在多个 switch 里同步改。这就是"状态爆炸"的起点。

Finite State Machines to the Rescue

有限状态机(FSM)是解决状态爆炸的正规工具:状态 + 事件 + 转换表。转换表把"当前状态 × 事件 → 下一状态"集中在一张表里,规则一目了然、可检查、可调试。FSM 的"有限"二字很重要——状态数量固定,行为可预测,这是它适合游戏 AI 与角色控制的原因。

Enums and Switches

最直接的 FSM 实现:枚举 + switch。enum State { STANDING, JUMPING, ... } + switch (state) { case JUMPING: ... }。简单、快、零对象开销——小状态机用它正合适。问题出现在状态变多时:switch 分支膨胀、转换规则散落、状态行为与主角类耦合。它是 State 模式的基线,也是多数游戏"够用就行"的选择。

The State Pattern

State 模式把 FSM 的每个状态变成一个StandingStateJumpingState。主角对象持有当前状态指针;handleInput()update() 委托给状态对象。转换 = 换状态指针。这样每个状态的行为、以及对转换的触发,都收进自己的类——加状态加类,改规则改对应类,主角类不再膨胀。

A state interface

State 模式需要一个接口:class State { virtual void handleInput(Player&, Input) = 0; virtual void update(Player&, float dt) = 0; }。所有具体状态实现这两个方法。主角类只认识这个接口——它不知道当前处于哪个具体状态,只把请求转发出去。接口是解耦的关键:主角不认识状态,状态之间也不互相认识(转换由状态内触发)。

Classes for each state

每个状态一个类,如 JumpingState::handleInput 里写"按↓则切换到 DivingState"。注意转换的写法:状态类要拿到主角的引用才能换状态指针(player.setState(new DivingState()))——这引入"状态类知道主角"的耦合,但也让转换规则随状态走,集中可查。各状态类互不依赖,是平行的。

Delegate to the state

主角的 handleInput 变成一行:state_->handleInput(*this, input)。主角不再自己判断"我在哪个状态所以怎么响应"——它把问题抛给当前状态对象。这就是"委托":把行为决策权交给状态。主角瘦身为纯容器,状态的增删不影响主角代码。

Where Are the State Objects?

状态对象放哪?两种选择:静态单例状态(每个状态只建一份,所有角色共享——状态无自身字段时最佳,省内存)与实例化状态(每个角色各自的状态对象,可携带每角色私有字段——状态需要记录该角色的专属进度时用)。后者更灵活,前者更省。作者强调:多数游戏状态不需要每角色独立实例,共享静态状态即可。

Static states

静态状态:static JumpingState instance; 每个状态类只有一个实例,所有角色共用。要求状态对象无字段或只有共享字段——如果状态需要记录"这个角色已经跳跃了多少毫秒",静态状态就不够用了(所有角色共享同一份)。适合纯行为状态(站立、攻击判定固定)的场景。

Instantiated states

实例化状态:每次进入某状态就 new 一个新对象,状态内可存该角色的私有数据(如跳跃计时、攻击连击数)。进入/退出时创建/销毁,配合进入动作和退出动作(见下节)使用。代价是频繁创建销毁——可用对象池缓解,也是状态模式与 Object Pool 的常见搭配。

Enter and Exit Actions

状态模式常配进入动作与退出动作:状态类增加 enter(Player&)exit(Player&),在切换时调用——进入时播放音效/初始化计时,退出时清理/结算。这解决了一个经典问题:"攻击状态结束时要收招"——收招逻辑不再散落在各个转换点,而是写在 AttackingState::exit() 里,集中管理。

What's the Catch?

State 模式的代价:状态类数量爆炸(每状态一个类,小状态机嫌重)、间接调用开销(每次委托多一层虚函数)、状态类要访问主角私有成员(要么 friend 要么公开 getter,破坏封装)。作者的建议:先用枚举 switch,状态多到 switch 膨胀再上状态模式——过早抽象是浪费。

Concurrent State Machines

同一对象可同时处于多个正交状态:比如角色同时"在地面/空中"(移动状态机)和"有武器/空手"(装备状态机)。方案:多个状态指针并存,每个管理一个维度。这样"空中换武器"的组合是两种状态机的笛卡尔积,而不用把 2×2 组合硬编码成 4 个状态——状态机正交化降低组合爆炸。

Hierarchical State Machines

层次状态机:把状态分组,子状态继承父状态的公共行为——"移动中"是父状态,"跑步/走路"是子状态;在跑步中收到"被打断"事件,若子状态不处理,冒泡给父状态处理。好处:公共行为只写一次(父状态),子状态只写差异。代价:事件冒泡规则增加理解成本。

Pushdown Automata

下推自动机:状态栈——进入子状态把父状态压栈,子状态结束时弹栈回父。典型场景:角色攻击中暂停,进入"暂停菜单"状态(压栈),关闭菜单弹栈恢复攻击——暂停时不需要记录"攻击到哪一步",栈本身保存了恢复点。适合"可中断-恢复"的行为流。

So How Useful Are They?

作者的总结:FSM 在游戏里极其常用,但完整的状态模式并不总是必要。枚举 switch 覆盖大量场景;状态模式在"状态多、转换复杂、行为差异大"时真正发光。层次状态机与下推自动机是高级变体,多数游戏用不上,但理解它们能让你在遇到"状态嵌套""可恢复中断"时知道有解。

可迁移实现或计算骨架

initial state -> 定义状态接口 -> 每个状态一个类 -> 主角委托给当前状态 -> 事件触发转换
fault injection -> 用枚举 switch 堆叠所有状态行为
pass condition -> 新增状态只加一个类、转换规则随状态集中
reset -> initial state

对应实现要点:状态接口声明 handleInput/update/enter/exit;状态类实现行为与转换;主角持有状态指针并把请求委托出去;进入/退出动作收进 enter/exit。该骨架只保存实验合同;真实项目还要固定状态清单、事件种类与转换表,并保留基线实现以便回退。

本章练习与节点验证矩阵

练习

问题 1:加状态对比。

操作:在动画状态机中"新增"一个"受伤硬直"状态

问题 2:转换集中验证。

操作:列出动画中全部转换(事件 → 目标状态),核对是否各自收在源状态类里

问题 3:正交状态。

操作:为角色增加"装备"维度状态机,与移动状态机并存

问题 4:栈恢复实验。

操作:用下推自动机模拟"攻击中打开暂停菜单再关闭"

术语复核

名词解释

本章出现的专业名词,用大白话再讲一遍。

状态
转换
状态机

练习答案参考

练习判据即答案:每个练习的"验证判据"列给出了通过标准——先自己动手,再对照判据核验。

术语复核与本章回顾

掌握"7. State"意味着能从"枚举 switch 让加状态改多处"出发,解释状态、转换、状态机与状态类四者的关系,再用"新增状态的成本、转换规则的集中度、状态行为的封装度"三个可观察量推翻或保留实现。若三个判据不能同时满足,本章仍未通过。

一句话回顾:把"当前在哪个状态"变成可替换的对象——行为随状态走、转换随状态走,加状态加类、改规则改类,主角类从此不再膨胀。

阅读导航

← 上一页:6. Singleton · 下一页:III. Sequencing Patterns →

← 上一页:6. Singleton · 下一页:III. Sequencing Patterns →

资料与写作方式声明

本章以Robert Nystrom《Game Programming Patterns》(游戏编程模式)权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…