联系方式
Java服务器开发群:66728073
游戏开发者高级群:398808948
Unity3d游戏开发:286114103
龙井,铁观音,茶叶
游戏系统设计开发之命令模式(二)
2016-10-07 16:42浏览数:760 

在游戏开发中使用命令模式的一个例子

在每个游戏中都有一块代码读取用户的输入——按钮按下,键盘敲击,鼠标点击,诸如此类。 这块代码会获取用户的输入,然后将其变为游戏中有意义的行为。

一种简单死了的实现会是这样:

void InputHandler::handleInput(){

 if (isPressed(BUTTON_X)) jump();

 else if (isPressed(BUTTON_Y)) fireGun();

 else if (isPressed(BUTTON_A)) swapWeapon();

 else if (isPressed(BUTTON_B)) lurchIneffectively();

}

这个函数通常在游戏循环中每帧调用一次,我确信你可以理解它做了什么。这代码在我们想将用户的输入和程序行为硬编码在一起时可以正常工作,但是许多游戏允许玩家设定按键的功能。为了支持这点,需要将这些对jump()和fireGun()的直接调用转化为可以更换的东西。 “更换”听起来有点像声明变量,因此我们需要表示游戏行为的对象。进入:命令模式。

先实现命令:

 

class JumpCommand: ICommand

   {

       public void execute()

       {

           //这此方法可能来自另外的类,这里只是表达一下调用的意思。

           jump();

       }

   }

   class FireCommand : ICommand

   {

       public void execute()

       {

           fireGun();

       }

   }

///下面省略几千行


构造一个控制器,在输入控制器,为每个键存储一个指向命令的引用,相当于前面说的Invoker

class InputHandler

   {

       private ICommand commandX;

       private ICommand commandY;

       private ICommand commandA;

       private ICommand commandB;

       //处理输入的操作

       public void handleInput()

       {

           if (isPressed(BUTTON_X)) commandX.execute();

           else if (isPressed(BUTTON_Y)) commandY.execute();

           else if (isPressed(BUTTON_A)) commandA.execute();

           else if (isPressed(BUTTON_B)) commandB.execute();

       }

//这是另外一个使用方式,与上面的是一样的,这里把command传出去

       ICommand InputHandler::handleInput()

       {

           if (isPressed(BUTTON_X)) return commandX;

           if (isPressed(BUTTON_Y)) return commandY;

           if (isPressed(BUTTON_A)) return commandA;

           if (isPressed(BUTTON_B)) return commandB;

           // Nothing pressed, so do nothing.

           return NULL;

       }

}


这是命令模式的简短使用。如果你能够看出它的好处,可以接着往下看了。

在实践中,这并不是常用的功能,但是这经常会有类似的用例跳出来。到目前为止,我们只考虑了玩家控制的角色,但是游戏中的其他角色呢?它们被游戏AI控制。我们可以在AI和角色之间使用相同的命令模式;AI代码只是简单的放出Command对象。在选择命令的AI和展现命令的游戏角色间解耦给了我们很大的灵活度。 我们可以对不同的角色使用不同的AI,或者为了不同的行为而混合AI。 想要一个更加有攻击性的同伴?插入一个更加有攻击性的AI为其生成命令。 事实上,我们甚至可以为玩家角色加上AI, 这在原型阶段,游戏需要自动驾驶员是很有用的。把控制角色的命令变为第一公民对象,去除直接方法调用中严厉的束缚。 取而代之的是,将其视为命令队列,或者是命令流:

一些代码(输入控制器或者AI)产生一系列指令然后将其放入流中。 另一些指令(调度器或者角色自身)消耗指令并调用他们。 通过在中间加入了一个队列,我们解耦了消费者和生产者。

撤销和重做

这个例子是这种模式最广为人知的使用情况。如果一个命令对象可以一件事,那么它亦可以撤销这件事。在一些策略游戏中使用撤销,这样你就可以回滚那些你不喜欢的操作。在人们创造游戏时,这是必不可少的工具。不能撤销肥手指导致的错误的编辑器,肯定会让游戏设计者恨你。

没有了命令模式,实现撤销非常困难,有了它,就是小菜一碟。假设我们在制作单人回合制游戏,想让玩家能撤销移动,这样他们就可以集中注意力在策略上而不是猜测上。我们已经使用了命令来抽象输入控制,所以每个玩家的举动都已经被封装其中。举个例子,移动一个单位的代码可能如下:

class MoveUnitCommand : ICommand

   {

       private int beforeX;

       private int beforeY;

      private int _x;

       private int _y;

       private Unit unit;

       

       public MoveUnitCommand(Unit unit,int x,int y)

       {

           this.unit = unit;

           this.beforeX = 0;

           this.beforeY = 0;

           this._x = x;

           this._y = y;

       }

       public void execute()

       {

           unit.moveTo(_x, _y);

       }

}


注意这和前面的命令有些许不同。在前面的例子中,我们需要从修改的角色那里抽象命令。在这个例子中,我们将命令绑定到要移动的单位上。这条命令的实例不是通用的“移动某物”指令;而是游戏回合中特殊的一次移动。

这集中展示了命令模式可以被使用的几种情况。在某些情况中,就像第一个例子,指令是可重用的对象,代表了可执行的事件我们早期的输入控制将其实现为一个命令对象,然后在按键按下时调用其execute()方法。

这里的命令更加特殊。它们代表了特定时间点能做的特定事件。这意味着输入控制代码可以在玩家下决定时创造一个实例。就像这样:

ICommand handleInput()

       {

           Unit unit = getSelectedUnit();

           int destY = 0;

           if (isPressed(BUTTON_UP))

           {

               // Move the unit up one.

               destY = unit.getY() - 1;

               return new MoveUnitCommand(unit, unit.getX(), destY);

           }

           if (isPressed(BUTTON_DOWN))

           {

               // Move the unit down one.

               destY = unit.getY() + 1;

               return new MoveUnitCommand(unit, unit.getX(), destY);

           }

           // Other moves...

           return NULL;

       }

指令是一次性意味我们很快的获得了一个优点。为了让指令可被取消,我们为每个类定义另一个需要实现的方法:

interface ICommand

   {

       void execute();

       void undo();

   }

undo()方法回滚了execute()方法造成的游戏状态改变。这里是添加了撤销后的移动指令:

class MoveUnitCommand : ICommand

   {

       private int beforeX;

       private int _x;

       private int _y;

       private int beforeY;

       private Unit unit;

       

       public MoveUnitCommand(Unit unit,int x,int y)

       {

           this.unit = unit;

           this.beforeX = 0;

           this.beforeY = 0;

           this._x = x;

           this._y = y;

       }

       public void execute()

       {

           this.beforeX = unit.getX();

           this.beforeY = unit.getY();

           unit.moveTo(_x, _y);

       }

       public void undo()

       {

           unit.moveTo(beforeX, beforeY);

       }

}

注意我们为类添加了更多状态。当单位移动时,它忘记了它之前是什么样的。如果我们想要撤销这个移动,我们需要记得单位之前的状态,也就是xBefore_yBefore_做的事。为了让玩家撤销移动,我们记录了执行的最后操作。当他们按下control+z时,我们调用命令的undo()方法。(如果他们已经撤销了,那么就变成了“重做”,我们会再一次执行命令。)

支持多层的撤销也不太难。我们不单单记录最后一条指令,还要记录指令列表,然后用一个引用指向“当前”的那个。 当玩家执行一条命令,我们将其添加到列表,然后将代表“当前”的指针指向它。


当玩家选择“撤销”,我们撤销现在的指令,将代表当前的指针往后退。 当他们选择“重做”,我们将代表当前的指针往前进,执行该指令。 如果在撤销后选择了新指令,那么指令列表中代表当前的指针所指之后的部分就全部清除了。

在使用设计模式的时候,我们要根据实际情况自己灵活改变,不要死套设计模式。孙子说:运用之妙,存乎一心也。

打赏

参考:http://www.cnblogs.com/konck/p/4199907.html

http://gameprogrammingpatterns.com/command.html

http://www.youxijishu.com/h-index.html

http://www.cnblogs.com/devinzhang/archive/2012/01/06/2315235.html