什么是命令模式?
命令模式是《游戏编程模式》中介绍的第一种设计模式,也是作者“最喜欢的设计模式”。它到底有什么魅力呢?
命令模式是一种行为型设计模式(Behavioral Pattern),和之前介绍的几种创建型设计模式有所不同。行为型模式是对在不同的对象之间划分责任和算法的抽象化,不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。
GoF对命令模式的定义如下:
命令模式将一个请求( request) 封装成一个对象,从而允许开发者使用不同的请求、队列或日志将客户端参数化,同时支持请求操作的撤销与恢复。”命令就是面向对象化的回调。
《游戏编程模式》的作者认为这个定义不够明确,因此在书中给出了一个更简单明朗的定义:
命令就是一个对象化(实例化)的方法调用(这通常涉及到委托/函数指针/闭包…)。
在游戏开发中,命令模式由三个部分组成:调用器(invoker),命令(command)和接收者(receiver)。
-
调用器
也成为“发送者”,负责对请求进行初始化,其中必须包含一个成员变量来存储对于命令对象的引用。
发送者触发命令,而不向接收者直接发送请求。发送者并不负责创建命令对象,它通常会通过构造函数从客户端处获得预先生成的命令。 -
命令
抽象的命令接口,包含了execute函数。 -
具体命令
具体命令,实现了execute函数,代表命令的具体行为逻辑。
优点
- 命令模式将请求与执行过程解耦,客户程序不需要知道一个命令具体会有什么行为。
- 可以这样创建宏(一系列命令),像管道一样执行命令流
- 可以在不修改现有代码的情况下增加新的命令
- 利用命令缓冲区(通常由队列维护)可以实现撤销、回放等操作
缺点
- 每增加一个新的命令,会增加一个类,这样会增加维护成本
- 因为我们将命令与客户程序解耦了。当发生错误或者异常的时候,如何正确处理返回返回值会变得棘手。在多线程环境下这个问题会更麻烦。
应用场景:输入绑定
通常来说,一个游戏会允许玩家自定义操作按键,例如有的玩家习惯用方向键控制角色移动,有的则习惯用wasd。上图展示了空洞骑士中的默认键位配置,玩家可以自行更改其中的任意一项。对现代版本的Unity来说,我们可以用新输入系统为每个操作(也就是命令,例如向上、跳跃等)绑定对应的按键,还可以通过代码更改绑定按键的值,实现自定义配置的功能。但在新输入系统发布之前,如何允许用户自行配置输入与操作的绑定或许是一个很头疼的功能。对初学者来说,可能会倾向于直接采用硬编码的方式ban掉这个功能:
1 | public class InputManager{ |
但如果我们将上下左右的命令封装成对象呢?
1 | // 命令接口 |
这就是命令模式的一个实例。在用户自定义绑定的时候,更改按键(和更改前按键)对应的命令即可。发送者返回了命令给接收者(在这里是游戏循环),这是“命令即具体化”的体现。
如何实现撤销?
前文中提到了很多关于“撤销”和“回放”的内容,这个需求在游戏中很常见,例如不小心点错了等等,用户习惯用ctrl+z来撤销做错的这一步(或多步)。上文的代码似乎并没有实现重做功能,该怎么做呢?其实原理也很简单,在每个具体命令下实现Undo方法定义如何重做即可:
1 | // 命令接口 |
然而需要注意的是,有些命令并不“基于上一步”,而是直接修改了状态,例如传送。这类型命令的重做就需要变量来“记住”原先的位置。总之,undo逆转了命令。
1 | // 传送命令 |
多次撤销
undo方法虽然实现了撤销的操作,但只执行了最近命令一次,如果想撤销刚才玩家执行的一系列命令该怎么办呢?我们可以维护一个队列和指向当前命令的指针来记录所有命令。
当在当前命令指针处执行了新的命令,当前指针的右侧将变成新的命令,同时回收掉原本后续的命令(相当于覆盖掉了)。有了这个队列,我们也可以轻易的实现“回放”功能,只需要从旧到新依次执行命令即可。在网络同步中,帧同步也是利用了类似的思想,逐帧地存储各个客户端发送的指令(也就是命令),因此适合战场回放一类的功能。
参考资料
[美] Robert Nystrom 尼斯卓姆. 游戏编程模式 (游戏设计与开发). 人民邮电出版社.
https://unity.com/cn/how-to/use-command-pattern-flexible-and-extensible-game-systems
https://design-patterns.readthedocs.io/zh-cn/latest/behavioral_patterns/command.html
https://refactoringguru.cn/design-patterns/command
才疏学浅难免有所疏漏,欢迎大家在评论区和我交流!