1
1
命令模式
2
2
============================
3
+ # 目的
3
4
4
5
命令模式是我最喜爱的模式之一。在我写过的许多大型游戏或者其他程序中,都有用到它。正确的使用它,会让你的代码变得更加优雅。对于这个模式,Gang of Four 有着一个预见性的深奥说明:
5
6
@@ -33,6 +34,8 @@ __命令就是一个对象化(实例化)的方法调用。__(A command is a re
33
34
34
35
但是这些都比较抽象和模糊。正如我所推崇的那样,我喜欢用一些具体点的东西来开头。为弥补这点,现在开始我将举例,它们都非常适合命令模式。
35
36
37
+ #动机
38
+
36
39
# 输入配置
37
40
38
41
每个游戏都有一处代码块用来读取用户原始输入-按钮点击,键盘事件,鼠标点击,或者其他等等。它记录每次的输入,并将之转换为游戏中一个有意义的动作(action):
@@ -131,6 +134,8 @@ void InputHandler::handleInput()
131
134
132
135
简而言之,这就是命令模式。如果你已经看到了它的优点,不妨看完本章的剩余部分。
133
136
137
+ # 模式
138
+
134
139
# 关于角色的说明
135
140
136
141
我们刚才定义的命令类在上个例子中是有效的,但他们很受限。问题在于,他们假设存在` jump() ` ,` fireGun() ` 等这样的能找到玩家的头像,使得玩家像木偶一样进行动作处理的顶级函数。
@@ -145,7 +150,7 @@ public:
145
150
virtual void execute(GameActor& actor) = 0;
146
151
};
147
152
```
148
- 这里,__ GameActor__ 是我们用来表示游戏世界中的角色的”游戏对象“类。我们将它传入` execute() ` 中,以便命令可以针对我们选择的角色进行调用 ,就像这样:
153
+ 这里,__ GameActor__ 是我们用来表示游戏世界中的角色的”游戏对象“类。我们将它传入` execute() ` 中,以便子类化的命令可以针对我们选择的角色进行调用 ,就像这样:
149
154
150
155
``` c++
151
156
class JumpCommand : public Command
@@ -182,7 +187,7 @@ if (command)
182
187
command->execute(actor);
183
188
}
184
189
```
185
- 假设` actor ` 是玩家角色的一个引用,这将会基于用户的输入来驱动角色,所以我们可以像第一个例子一样给角色赋予相同的行为。在命令和角色之间加入的间接层赋予了我们这样的能力:我们可以让玩家控制游戏中的任何角色,只需通过改变命令执行时传入的角色对象 。
190
+ 假设` actor ` 是玩家角色的一个引用,这将会基于用户的输入来驱动角色,所以我们可以赋予角色与前例一致的行为。在命令和角色之间加入的间接层使得我们可以让玩家控制游戏中的任何角色,只需通过改变命令执行时传入的角色对象即可 。
186
191
187
192
在实践中,这并不是一个常见的功能,但是有一种情况却经常见到。迄今为止,我们只考虑了玩家驱动角色(player-driven character),但是对于游戏世界中的其他角色呢?他们由游戏的AI来驱动。我们可以使用相同的命令模式来作为AI引擎和角色的接口;AI代码部分提供命令(Command)对象用来执行。(译者注:` command->execute(AI对象); ` )
188
193
@@ -208,13 +213,13 @@ AI选择命令,角色执行命令,它们之间的解耦给了我们很大的
208
213
209
214
# 撤销和重做(Undo and Redo)
210
215
211
- 最后这个例子(译者注:作者指的是撤销和重做)是命令模式最有名的应用了 。如果一个命令对象可以 _ do_ 一些事情,那么应该可以很轻松的 _ undo_ (撤销) 它们。撤销这个行为经常在一些策略游戏中见到,在游戏中如果你不喜欢的话可以回滚一些步骤。在_创建_游戏时这是一个很常见的工具。如果你想让你的游戏设计师们讨厌你,最可靠的办法就是在关卡编辑器中不要提供撤销命令,让他们不能撤销不小心犯的错误。
216
+ 最后这个例子(译者注:作者指的是撤销和重做)是命令模的成名应用了 。如果一个命令对象可以 _ do_ 一些事情,那么应该可以很轻松的 _ undo_ (撤销) 它们。撤销这个行为经常在一些策略游戏中见到,在游戏中如果你不喜欢的话可以回滚一些步骤。在_创建_游戏时这是一个很常见的工具。如果你想让你的游戏设计师们讨厌你,最可靠的办法就是在关卡编辑器中不要提供撤销命令,让他们不能撤销不小心犯的错误。
212
217
213
218
> 注解
214
219
215
220
> 这里可能是我的经验之谈。
216
221
217
- 如果没有命令模式,实现撤销是很困难的。有了它,小菜一碟啊。我们假定一个情景,我们在制作一款单人,回合制的游戏 ,我们想让我们的玩家能够撤销一些行动以便他们能够更多的专注于策略而不是猜测。
222
+ 如果没有命令模式,实现撤销是很困难的。有了它,小菜一碟啊。我们假定一个情景,我们在制作一款单人回合制的游戏 ,我们想让我们的玩家能够撤销一些行动以便他们能够更多的专注于策略而不是猜测。
218
223
219
224
我们已经可以很方便的使用命令模式来抽象输入处理,所以每次对角色的移动要封装起来。例如,像下面这样来移动一个单位:
220
225
@@ -240,9 +245,9 @@ private:
240
245
```
241
246
注意到这个和我们上一个命令不太相同。在上个例子中,我们想要_抽象_出命令,执行命令时可以针对不同的角色。在这个例子中,我们特别希望将命令_绑定_到移动的单位上。这个命令的实例不是一般性质的”移动某些物体“这样适用于很多情境下的的操作,在游戏的回合次序中,它是一个特定具体的移动。
242
247
243
- 这凸显了命令模式在实现时的一个变化。在某些情况下,像我们的第一对的例子,一个命令代表了一个可重用的对象,表示_ a thing that can be done_(一件可以被完成的事情)。我们前面的输入处理程序隐式的针对一个单一的命令对象,并要求在右按钮被按下的时候其 `execute()`方法被调用。
248
+ 这凸显了命令模式在实现时的一个变化。在某些情况下,像我们的第一对的例子,一个命令代表了一个可重用的对象,表示_ a thing that can be done_(一件可完成的事情)。我们前面的输入处理程序仅针对单一的命令对象,并要求在对应按钮被按下的时候其 `execute()`方法被调用。
244
249
245
- 这里,这些命令更加具体。他们表示_a thing that can be done at a specific point in time_(一件可以在特定时间点完成的事情 )。这意味着每次玩家选择移动,输入处理程序代码都会创建一个命令实例。像下面这样:
250
+ 这里,这些命令更加具体。他们表示_a thing that can be done at a specific point in time_(一件可在特定时间点完成的事情 )。这意味着每次玩家选择移动,输入处理程序代码都会创建一个命令实例。像下面这样:
246
251
247
252
```c++
248
253
Command* handleInput()
@@ -267,7 +272,7 @@ Command* handleInput()
267
272
return NULL;
268
273
}
269
274
```
270
- 事实上,命令的一次性使用将会是我们可以利用的一个优势 。为了撤销命令,我们定义了一个操作,每个命令类都需要来实现它:
275
+ 一次性命令的特质很快能为我们所用 。为了撤销命令,我们定义了一个操作,每个命令类都需要来实现它:
271
276
272
277
``` c++
273
278
class Command
@@ -318,13 +323,13 @@ private:
318
323
int x_ , y_ ;
319
324
};
320
325
```
321
- 注意到我们在类中添加了一些状态。当单位移动时,它会忘记它刚才在哪。如果我们要撤销移动,我们得记住单位的上一次位置 ,正是`xBefore_`和`yBefore_`变量的功能。
326
+ 注意到我们在类中添加了一些状态。当单位移动时,它会忘记它刚才在哪。如果我们要撤销移动,我们得记录单位的上一次位置 ,正是`xBefore_`和`yBefore_`变量的功能。
322
327
323
328
> 注解
324
329
325
- > 这看起来挺像[备忘录模式](http://en.wikipedia.org/wiki/Memento_pattern)的,但是我发现备忘录模式用在这里并不能有效的工作。因为命令试图去修改一个对象状态的一小部分,为对象的其他数据快照是浪费内存。只手动存储改变的位码相对来说就廉价多了 。
330
+ > 这看起来挺像[备忘录模式](http://en.wikipedia.org/wiki/Memento_pattern)的,但是我发现备忘录模式用在这里并不能有效的工作。因为命令试图去修改一个对象状态的一小部分,而为对象的其他数据创建快照是浪费内存。只手动存储被修改的部分相对来说就节省很多内存了 。
326
331
327
- > [持久化数据结构](http://en.wikipedia.org/wiki/Persistent_data_structure)是另外一个选择 。通过它们,每次对一个对象进行修改都会返回一个新的对象,保留原对象不变。通过这样明智的实现,这些新对象与原对象共享数据,所以比克隆整个对象的代价要小的多 。
332
+ > [持久化数据结构](http://en.wikipedia.org/wiki/Persistent_data_structure)是另一个选择 。通过它们,每次对一个对象进行修改都会返回一个新的对象,保留原对象不变。通过这样明智的实现,这些新对象与原对象共享数据,所以比拷贝整个对象的代价要小的多 。
328
333
329
334
> 使用持久化数据结构,每个命令存储着命令执行前对象的一个引用,所以撤销意味着切换到原来老的对象。
330
335
@@ -336,27 +341,29 @@ private:
336
341
337
342
当玩家选择”撤销“时,我们撤销当前的命令并且将当前的指针移回去。当他们选择”重做“,我们将指针前移然后执行命令。如果他们在撤销之后选择了一个新的命令,列表中位于当前命令之后的所有命令被舍弃掉。
338
343
339
- 我第一次在一个关卡编辑器中实现了这一点,我感觉自己就像一个巫师一样。我很惊讶它是如此的简单而且效用是如此的好。我们需要一个规则来确保每个数据的更改会经由一个命令来实现,但一旦你这样做了,剩下的就容易了 。
344
+ 我第一次在一个关卡编辑器中实现了这一点,顿时自我感觉良好。我很惊讶它是如此的简单而且高效。我们需要指定规则来确保每个数据的更改都经由一个命令实现,但只要定了规则,剩下的就容易得多 。
340
345
341
346
> 注解
342
347
343
348
> 重做在游戏中并不常见,但回放(re-play)却不是。一个很老实的实现方法就是记录每一帧的游戏状态以便能够回放,但是这样会使用大量的内存。
344
349
345
350
> 相反,许多游戏会记录每一帧每个实体所执行的一系列命令。为了回放游戏,引擎只需要运行正常游戏的模拟,执行预先录制的命令。
346
351
352
+ #设计决策
353
+
347
354
# 类风格化还是函数风格化?
348
355
349
356
此前,我说命令(commands)和第一类函数或者闭包相似,但是这里我举的每个例子都用了类定义。如果你熟悉函数式编程,你可能想知道如何用函数式风格实现命令模式。
350
357
351
- 我用这种方式写例子是因为 C++ 对于第一类函数的支持非常有限。函数指针无须过多阐述,仿函数(译者注:关于仿函数可以看[百科的介绍](http://baike.baidu.com/view/2070037.htm?fr=aladdin))看起来比较怪异,还需要定义一个类,C++11 中的闭包使用起来比较棘手因为要手动管理内存的缘故 。
358
+ 我用这种方式写例子是因为 C++ 对于第一类函数的支持非常有限。函数指针无须过多阐述,仿函数(译者注:关于仿函数可以看[百科的介绍](http://baike.baidu.com/view/2070037.htm?fr=aladdin))看起来比较怪异,还需要定义一个类,C++11 中的闭包使用起来比较棘手因为要手动管理内存 。
352
359
353
360
这并不是说在其他语言中你不应该使用函数来实现命令模式。如果你使用的语言中有闭包的实现,无论怎样,使用它们!在某些方面(In some ways),命令模式对于没有闭包的语言来说是模拟闭包的一种方式。
354
361
355
362
> 注解
356
363
357
364
> 我说在某些方面(In some ways),是因为即使在有闭包的语言中为命令构建实际的类或结构仍然是有用的。如果你的命令有多个操作(如可撤销命令),映射到一个单一函数是比较尴尬的。
358
365
359
- > 定义一个实际的附带字段的类也有助于读者很容易分辨该命令中包含哪些数据。闭包自动包装一些状态是比较简洁,但他们太过于自动化了以至于很难分辨出它们实际上持有的状态 。
366
+ > 定义一个实际的附带字段的类也有助于读者很容易分辨该命令中包含哪些数据。闭包自动包装一些状态是比较简洁,但它们太过于自动化了以至于很难分辨出它们实际上持有的状态 。
360
367
361
368
举个例子,如果我们在用 JavaScript 编写游戏,我们可以像下面这样创建一个单位移动命令:
362
369
0 commit comments