【编程模式】(一) ------ 命令模式 和 “重做” 及 “撤销”
前言
本文及以后该系列的篇章都是本人对 《游戏编程模式》这本书的阅读理解,从中对一些原理,用更直白的语言描述出来,并对部分思路或功能进行初步实现。而本文所描述的 命令模式, 相信读者应该都有了解过或听说过,如果尚有疑惑的读者,我希望本文能对你有所帮助。
命令模式是设计模式中的一种,但该系列所指的编程模式并非是指设计模式,设计模式只是一本分,现在我们先来探讨一下命令模式吧。
一. 为什么要用命令模式
在我解释什么是命令模式之前,我们先弄明白为什么要使用命令模式?
相信大家都玩过不少游戏,在游戏中,必不可少的就是游戏与玩家的交互,键盘的输入、鼠标的输入、手柄的输入等等,比如常见的这种
我们先简化一下,使用下面这种
在我们实现类似的功能时,我们的第一想法一般是
在这种情况下,我们很显然可以发现两个问题:
现在的游戏大部分都支持用户(玩家)手动配置按钮映射,毕竟每个人的习惯不一而至。在这种 情况下,很明显我们没办法更改按钮映射,所以我们需要一个 中间变量(命令) 来管理按钮行为。比如,设这个中间变量为 Temp ,默认情况下按下A键后,生成一个 Temp , Temp 会索引到 Attack(),然后执行;现在我们更改按钮配置,改为按下B键,生成同样的 Temp。同样执行 Attack()。这样,通过增加一层间接调用层,我们就可以实现命令的分配。
上述的 Attack() ,Jump(),这种顶级函数,我们一般都会默认是对游戏主角进行操作,也就是说这种情况下一条命令对应着一条对主角操作信息,这样,命令的使用范围就会被限制,而如果我们向这条命令传进一个对象,就可以实现类似 对象.Jump() 。可以明确的是,当游戏玩家和NPC(AI)执行同一种动作时,如 Attack(),即便他们的具体实现不一定相同,但我只需要同一条命令,传入不同的对象即可。
针对这两个问题,我们会发现,采用命令模式去处理按钮与行为之间的映射会更加的方便与高效。
二. 什么是命令模式
说了这么久,我们该说说这个所谓的命令模式究竟是个什么东西吧?
介绍:请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
目的:将一个请求封装成一个对象,从而可以用不同的请求对客户进行参数化。简洁一点,就相当于:我构建出一个 AttackCommond 类,这个类里面封装了角色进行攻击的函数;现在我把这个类实例化出来,然后通过实例化出的对象来调用其中的函数。
主要解决:行为的请求者与实现者通常是紧耦合关系,在需要进行 “记录” 的场合下比如 “撤销与重组”,这种紧耦合关系就会不适用,所以我们需要进行解耦。
优点:1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。
缺点:使用命令模式可能会导致某些系统有过多的具体命令类。
我们可以使用命令模式来作为 AI 引擎和角色(NPC)之间的接口,对不同的角色可以提供不同的命令;同样的,我们也可以把这些 AI 命令使用到玩家角色上,这就是大家都十分熟悉的演示模式(Demo Mode),即游戏中我们常见的自动战斗。想象一下,其实无论是玩家角色还是NPC,都是执行一样的命令,普通攻击 -> 满足一定条件后释放技能。所以我们可以使用同样的命令,分别传入玩家和NPC的对象,就可以初步实现这个功能。
三. 部分思路代码实现
我们先用C++的代码来说明思路:
先定义一个命令的基类
复制代码
1 class Command
2 {
3 public:
4 virtual ~Command(){}
5 virtual void execute(GameActor& actor)(){}
6 }
复制代码
然后给角色实现跳跃行为,定义一个跳跃命令类
复制代码
1 class JumpCommond : public Command
2 {
3 public:
4 JumpCommond();
5 ~JumpCommond();
6 virtual void execute(GameActor& actor)
7 {
8 actor.Jump();
9 }
10 };
复制代码
根据不同的按钮,返回不同的命令,然后根据返回的命令,传入适当的对象,执行命令
复制代码
1 Command* command = InputManager();
2 if(command)
3 {
4 command->execute(actor);
5 }
复制代码
这样大概就是一个基于命令模式的按钮映射流程。
四. 撤销与重做
撤销与重做是我们再常见不过的一个功能,如果我们不了解命令模式,我们会怎样实现这个功能?把每个步骤的前后状态保存成一个对象或者数据?通过覆盖该对象(数据)来实现前后状态的转换?这种对象(数据)该如何定义?又该如何存储?相信我们会被这些问题搞得头痛不已。
而撤销与重做则是命令模式的一个经典应用。对于任一个单独的命令来说,做(do)是可以实现的,那么 不做(undo) 理应也是可以实现的。以命令模式为基础,对方法进行封装,通过对 Do 和 Undo 的执行,使得对象在不同状态间进行切换,就是常见的撤销与重做功能。
以经典的位置移动为例:
定义命令
复制代码
1 class Command
2 {
3 public:
4 virtual ~Command(){}
5 virtual void execute(GameActor& actor) = 0;
6 virtual void undo() = 0;
7 }
复制代码
定义移动命令
复制代码
1 class MoveUnitCommond : public Command
2 {
3 public:
4 MoveUnitCommond(Unit* unit,int x,int y) : unit_(unit),x_(x),y_(y),beforeX(0),beforeY(0)
5 {
6
7 }
8 ~ MoveUnitCommond();
9 virtual void execute()
10 {
11 beforeX = unit_->x();
12 beforeY = unit_->y();
13 unit_->move(x_,y_);
14 }
15 virtual void undo()
16 {
17 unit_->move(beforeX,beforeY);
18 }
19 private:
20 Unit* unit_;
21 int x_;
22 int y_;
23 int beforeX;
24 int beforeY;
25 };
复制代码
其中,unit 为移动单位,beforeX,beforeY用来记录单位移动前的位置信息,执行 undo 时,即相当于把 unit 移动至原来的位置
以下面例子做说明,物体从 A 移动到 B,再从 B 移动到 C
这个过程物体执行了两个命令
命令1 命令2
Do 从A移动到B 从B移动到C
Undo 从B移回到A 从C移回到B
我们应该用一个栈或链表来存储这些命令,并且提供一个指针或引用,来明确指向 “当前” 命令。要注意的是,边界问题。
当物体处于C位置时,此物体理应可以执行 Undo ,但不可以执行 Do 方法,因为此时物体已经执行过了一次命令2的 Do 方法,当前指针指向命令2,且命令2后没有新的命令,即 “Do 已经到了尽头”;同理,当物体处于 A 时,同样不可以执行 Undo 方法。读者要十分注意这个问题,不要混淆。
为了更直观地体验到命令模式实现的撤销与重做,我用 Unity 做了个演示,熟悉 Unity 的读者可以动手实现一下。
I. 创建一个 Capsule 作为主角;创建两个 Button 作为前进后退按键
II. 创建三个类
1. 游戏角色类,这里我并不需要什么属性,所以这里是个空类,读者可以自行定义
复制代码
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4
5 public class GameActor : MonoBehaviour
6 {
7
8 }
复制代码
2.命令类
先定义基类
复制代码
1 public class Commond
2 {
3 public virtual void execute() { }
4 public virtual void undo() { }
5 }
复制代码
在此基础上,定义一个移动命令类
复制代码
1 public class MoveCommond : Commond
2 {
3 private float _x;
4 private float _y;
5 private float _z;
6
7 private float _beforeX;
8 private float _beforeY;
9 private float _beforeZ;
10
11 private GameActor gameActor;
12
13 public MoveCommond(GameActor GA,int x,int y, int z)
14 {
15 _x = x;
16 _y = y;
17 _z = z;
18 _beforeX = 0;
19 _beforeY = 0;
20 _beforeZ = 0;
21 gameActor = GA;
22 }
23
24 public override void execute()
25 {
26 _beforeX = gameActor.transform.position.x;
27 _beforeY = gameActor.transform.position.y;
28 _beforeZ = gameActor.transform.position.z;
29
30 gameActor.transform.position = new Vector3(_beforeX + _x, _beforeY + _y, _beforeZ + _z);
31 base.execute();
32 }
33
34 public override void undo()
35 {
36 gameActor.transform.position = new Vector3(_beforeX , _beforeY , _beforeZ);
37 base.undo();
38 }
39 }
复制代码
代码的作用和前文所说的几乎一致
3. 定义一个命令管理类
先定义一个 List 来存储命令,并对我们所需要的元素初始化
复制代码
1 private List CommondList = new List();
2 private GameActor gameActor;
3 private Commond commond = new Commond();
4 private int index;
5 private Button Backward;
6 private Button Forward;
7
8 private void Start()
9 {
10 gameActor = GameObject.Find("Capsule").GetComponent();
11 Backward = GameObject.Find("Canvas/Backward").GetComponent