2. Command
2. Command:把输入动作对象化后再决定目标、执行时刻、撤销和重放,通 过可复位因果实验和反例证据验收。
2. Command
学习目标
- 能画出 Command 模式的类关系
- 能解释命令对象如何支持重绑定与撤销
- 能用历史栈实现命令回放
为什么"2. Command"从问题证据开始
最直觉的输入处理是:按键按下 → 直接调用角色方法 player.jump()。这段代码今天能跑,但明天你遇到三个需求就卡住了——按键重绑定(玩家想用空格跳、用 X 跳、用手柄跳)、输入回放(录一段操作重放给 AI 或测试)、撤销(按错键想退回上一步)。这三个需求有一个共同点:它们都需要"把一次输入变成一个可以传递、存储、重放的东西",而直接调用方法做不到。
本页把"无模式基线"定义为"按键直接调用方法"的代码,然后引入命令对象作为候选机制,验证它是否真的让重绑定、回放、撤销三个需求变成可实现。通过条件是同一段输入流可以重绑定到不同按键、可以被录制重放、可以撤销——而不是接口数量增加。
🔮 先预测:让 Player 执行 3 个命令再逐个撤销,状态能否精确回到初始?
来源、版本与独立重写边界
本页用作者完整在线正文核对正式标题、设计分叉和时代语境,并以作者源码仓库交叉检查结构。仓库许可证明确正文、HTML与样式为 CC BY-NC-ND 4.0,示例程序等其他文件为 MIT;因此下列中文解释、图示、交互和代码均为独立教学重写,不翻译、拼接或改写受 ND 限制的原文表达。
本章机制与术语
理解 Command 需要四个核心概念:
- ↡:把"一次动作"封装成对象,含接收者引用与参数,暴露统一的 execute() 接口。
- ↡:持有命令并决定何时执行它的对象——输入系统、AI、回放器都是调用者。
- ↡:知道如何执行真实操作的对象(如 Player),命令只做转发。
- ↡:调用者不认识接收者,只认识命令接口——这是重绑定、回放、撤销三种能力共同的根基。
四个概念共同约束"把输入动作对象化后再决定目标、执行时刻、撤销和重放":任何结论都必须回到"命令能否被重绑定、能否被存储重放、能否被逆序撤销"三个可观察量。
Command — 把动作变成可传递的对象
第 1 / 6 步 · ① 输入事件到达 InputHandler:handleInput() 把按键转成命令对象
命令把「按哪个键」与「做什么」拆开:改键位、录回放、做撤销,都只是对命令对象的不同处理。
💡 对着动画看:动画左栏是类关系(interface 虚框 + 实现类 + 调用者/接收者),右栏是执行管道——注意 JumpCommand 卡片沿泳道从 InputHandler 滑向 Player 的过程。这就是"命令即票据":输入被转成票据,票据被传递、被记录,最后接收者按票据执行。读"撤销与重做"一节时,想想如果每个票据都留一份底单(历史栈),撤销就只是逆序结算底单。
官方结构逐项深读
2. Command
Command 模式的核心主张是:把"做什么"从"什么时候做、谁来做"中分离。一次按键不再直接调用 jump(),而是先构造一个 JumpCommand 对象(此刻还不知道何时执行),再由某个机制决定何时调用它的 execute()。这个看似多余的一步,换来的是输入系统的全部灵活性。
Configuring Input
输入配置是 Command 的第一个直接收益:按键表从"按键 → 方法"变成"按键 → 命令对象"。要重绑定,只需换表项——KEY_SPACE → JumpCommand 改成 KEY_X → JumpCommand,改的是数据不是代码。玩家按键时输入系统查表,构造对应命令,交给执行机制。游戏关卡甚至可以动态换键位表,做出"换一套操作方式"的玩法。
Directions for Actors
命令让"操作"与"执行者"解耦:同一个 MoveCommand(left, 2) 可以作用到任何实现了移动接口的角色——玩家角色、NPC、可操控的物体。这带来两个能力:一是统一操作接口(所有角色响应同一套命令),二是AI 也能发命令(AI 决策器构造命令对象,就像玩家输入一样),玩家操作和 AI 操作从此走同一条管道。
Undo and Redo
撤销是命令模式最有说服力的价值:既然每次动作都是对象,就可以把它们按执行顺序存进历史栈;撤销 = 弹栈并调用该命令的 undo()(逆操作),重做 = 再压栈并 execute()。注意 undo 不是"删掉刚才的修改"而是"执行相反的修改"——MoveCommand 撤销就是往反方向移动。实现前提:每个命令必须同时提供 execute 和 undo 两个操作,这反过来迫使你把动作设计得可逆。
Classy and Dysfunctional?
命令对象也常被批评"啰嗦":每个动作一个类,一个简单的跳跃要建类、要 new 对象、要查表——比起直接调用方法麻烦得多。但麻烦换来的是架构级能力(回放、撤销、重绑定、网络同步)。折中方案:命令不一定是类,可以是函数/闭包、或数据(类型+参数的元组)。关键是**"动作被封装成可传递的值"**,至于封装成什么形式,随语言和场景而定。
See Also
- Update Method:命令决定"做什么动作",Update Method 决定"实体每帧怎么自己推进"——前者是外部触发的动作,后者是内在持续的更新,两者互补。
- Event Queue:命令的执行时机可以交给事件队列解耦——输入到达先入队,主循环空闲时再执行,避免某帧命令风暴。
- 状态机(State):命令适合"一次性动作",状态适合"持续状态"——跳跃是命令(执行一次),而"跳跃中"的状态由状态机管理。
可迁移实现或计算骨架
initial state -> 输入事件 -> 查表构造命令 -> 命令入历史栈 -> execute() -> 接收者行动
fault injection -> 按键直接调用角色方法,跳过命令对象
pass condition -> 同一输入流可重绑定、可录制重放、可撤销
reset -> initial state对应实现要点:输入系统持有一个"按键 → 命令工厂"的表;每帧把事件转成命令对象压栈并 execute;撤销时弹栈调用 undo。该骨架只保存实验合同;真实项目还要固定输入设备的键位集合、历史栈上限与重放帧率,并保留基线实现以便回退。
本章练习与节点验证矩阵
练习
问题 1:重绑定实验。
操作:在动画中想象把"空格"换成"X"键触发 JumpCommand
问题 2:撤销实验。
操作:让 Player 依次执行 3 个命令,再逐个 undo
问题 3:回放实验。
操作:把一局输入的命令序列保存,重放到空场景
术语复核
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 命令对象
- 调用者
- 接收者
练习答案参考
练习判据即答案:每个练习的"验证判据"列给出了通过标准——先自己动手,再对照判据核验。
术语复核与本章回顾
掌握"2. Command"意味着能从"按键直接调用方法让重绑定、回放、撤销互相缠绕"出发,解释命令对象、调用者、接收者与解耦四者的关系,再用"命令能否被重绑定、重放、撤销"三个可观察量推翻或保留实现。若同一输入流不能同时支持三种操作,本章仍未通过。
一句话回顾:把"按了哪个键"和"做了什么"拆开——键位表管映射、命令对象管动作、历史栈管撤销,三者各司其职,游戏输入从此任你摆布。