游戏设计模式--命令模式

命令模式

命令模式是我最爱的设计模式之一。我的大部分游戏项目里面,或多或少的都有命令模式在里面。当我们把命令模式用在合适的地方的时候,它能灵巧的解耦一些粗糙的代码。对于如此漂亮的模式,“四人帮”里面有一段深奥的描述:

Encapsulate a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operations.

上面这段很深奥。。。。

而我的精炼理解,命令模式就是具体化的函数调用。

当然精炼的话语,有的时候很难理解和产生作用。我们这里,换一种说法,回调函数,函数指针,在四人帮的户籍里面,最后说到:

命令模式就是回调函数在面向对象里面的代替品

这句话,我想应该比最上面的话要好理解很多了。

但是无论如何这些都是很抽象的语句。下面我们会用具体的例子来理解命令模式,到底是什么,它又有什么聪明之处呢?

配置输入

基本上,大多数游戏里面,我们都会去读取玩家的原始输入。像,手柄按键,键盘按键,鼠标点击等等。然后,把这些输入转换为有意义的游戏行为:

 

一个不好的,但是很简单的实现方式如下:

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 Command

{

public:

  virtual ~Command() {}

  virtual void execute() = 0;

};

然后我们去创建具体的游戏行为子类:

class JumpCommand : public Command

{

public:

  virtual void execute() { jump(); }

};

class FireCommand : public Command

{

public:

  virtual void execute() { fireGun(); }

};

// You get the idea...

在我们的输入处理里面,我们存储每一个按键对应的命令指针:

class InputHandler

{

public:

  void handleInput();

  // Methods to bind commands...

private:

  Command* buttonX_;

  Command* buttonY_;

  Command* buttonA_;

  Command* buttonB_;

};

现在我们输入处理,只需要代理给那些命令指针即可:

void InputHandler::handleInput()

{

  if (isPressed(BUTTON_X)) buttonX_->execute();

  else if (isPressed(BUTTON_Y)) buttonY_->execute();

  else if (isPressed(BUTTON_A)) buttonA_->execute();

  else if (isPressed(BUTTON_B)) buttonB_->execute();

}

之前我们的自己调用,现在变成了间接调用关系:

 

这就是一个命令模式的基本外壳。如果你已经看出来了它的优势的话。可以思考下章节后面的一部分。

执行者的直接调用

上面我们设计的命令模式,其实还是有限制的。它的问题在于,我们前面的命令里面执行的上层函数jump(),fireGun()等等,都默认的认为,他们自己知道如何找到执行主体(哪一个角色),从而让角色像牵线木偶一样跳起舞来。

然后这种假设去限制了我们命令模式的灵活性。现在的情况下,我们的JumpCommand只能够让我们主角跳起来。让我们来解开这个限制吧。我们换一个调用方式,我们将执行者作为参数传递到我们命令中:

class Command

{

public:

  virtual ~Command() {}

  virtual void execute(GameActor& actor) = 0;

};

这里,GameActor就是我们的游戏角色,我们将它传递给execute(),然我们命令可以选择不同角色去执行:

class JumpCommand : public Command

{

public:

  virtual void execute(GameActor& actor)

  {

    actor.jump();

  }

};

现在,我们就可以用一个类去处理游戏里面所有角色的行为了。我们的代码需要做一下修改,首先将我们的handleInput()修改为返回命令。

Command* InputHandler::handleInput()

{

  if (isPressed(BUTTON_X)) return buttonX_;

  if (isPressed(BUTTON_Y)) return buttonY_;

  if (isPressed(BUTTON_A)) return buttonA_;

  if (isPressed(BUTTON_B)) return buttonB_;

 

  // Nothing pressed, so do nothing.

  return NULL;

}

它不能直接执行命令了,因为它并不知道执行者是谁。这里我们其实已经看出来了,命令模式是一个具体的函数调用了。我们可以延时调用命令。

然后,我们就可以获得命令,并在知道执行者的情况下,执行对应的命令了。

Command* command = inputHandler.handleInput();

if (command)

{

command->execute(actor);

}

如果这里的actor是引用的主玩家角色,那么这里就可以按照角色的输入正确的驱动他的行为了,就像回到了我们的第一个例子一样。但是我们加入的命令中间层,让我们的命令和执行者分离,让我们拥有了更加灵活的能力:我们现在可以通过换不同的actor参数来控制不同的角色了

实际上,这个并不是通用的作用,但是经常会有这种需求的出现。现在我们先考虑角色。我们驱动了主角,但是游戏世界的其他角色呢?(npcmonster等等)这些,都是靠着AI去驱动的。现在我们就可以使用同一套命令模式去适配AI引擎逻辑和角色了。AI代码相当于就是简单的抛出Command对象就可以了。

我们让AI可以选择命令和不同的Actor,这样就可以让我们的代码更加的灵活。我们可以使用不用的AI模块对应不同的角色。想要更加强大的AI对手,我们只需要修改AI,产生跟多的命令既可以。事实上,我们甚至可以将AI的产生的命令发送给玩家角色,就可以做一些自动战斗和一些Demo剧情了。

去除到AIActor的直接调用后,我们可以将命令想想成一个流化的过程了:

 

一些代码(输入或者是AI),产生命令,然后将命令放入到流里面,其他的代码获取命令,然后选择actor去执行。这样通过中间命令的连接,我们将生产者和消费者,分割到了2端。

如果我们让命令序列化,那么,我们就可以将整个流在网络上面转发,在另一个机器上面执行,这个也是多人网络游戏非常重要的一部分。同时记录这些命令也可以让我们实现重放的功能。

撤销和重做

最后一个例子,是命令模式最有名的一个功能。如果一个命令可以让对象做某件事情,那么也可以做一些事件让对象回到之前的状态。撤销通常在策略游戏里面实现一些你做错了,想回滚的操作。在游戏编辑工具中,则更是常用的了。如果让游戏关卡设计师,用的你的工具开发游戏,而又没有撤销功能的话,我想你会被打的。。。

如果不适用命令模式,实现撤销功能会很非常难。但是使用命令模式的话,那就是一件非常轻松的事情了。现在我们假象我们在做一款单机,回合制的游戏,我们想要玩家可以撤销他之前的移动操作,让玩家可以更多的在思考策略上面,而不是猜测上面。

前面我们已经通过命令模式去抽象了输入。现在玩家的移动也很容易的就实现了。例如:让玩家移动一个单位:

class MoveUnitCommand : public Command

{

public:

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

  : unit_(unit),

    x_(x),

    y_(y)

  {}

 

  virtual void execute()

  {

    unit_->moveTo(x_, y_);

  }

private:

  Unit* unit_;

  int x_, y_;

};

需要注意的是,这里的命令函数和之前的有一些不一样。前面的例子里面我们想要在命令中把actor抽象出来,可以替换。在这个例子里面我们想要将被移动的对象和命令绑定在一起。这里的移动命令不是一个通用的命令,他只是这种类型游戏的特殊移动命令。

这里其实也是一种命令模式实现的变种。在某些例子中,就像我们前面的实例,一个命令就是一个可以被多对象复用的命令,只需要实例化一个命令就可以了。我们前面的输入就是只创建了对应的一个命令,然后给所有需要使用的对象使用一下(execute())就可以了。

现在,我们的命令就比较特殊了。它代表一个在特定时间特对对象实现的某个操作。也就是说,这个命令在需要的是,都需要创建(new)一个新的命令对象出来,如下:

Command* handleInput()

{

  Unit* unit = getSelectedUnit();

  if (isPressed(BUTTON_UP)) {

    // Move the unit up one.

    int destY = unit->y() - 1;

    return new MoveUnitCommand(unit, unit->x(), destY);

  }

 

  if (isPressed(BUTTON_DOWN)) {

    // Move the unit down one.

    int destY = unit->y() + 1;

    return new MoveUnitCommand(unit, unit->x(), destY);

  }

 

  // Other moves...

 

  return NULL;

}

事实上,现在之类每次使用命令都创建一个新的命令出来的方式,在我们实现后面的撤销功能中,是必不可少的。为了实现撤销,我们需要在命令中定义另一个通用的函数:

class Command

{

public:

  virtual ~Command() {}

  virtual void execute() = 0;

  virtual void undo() = 0;//撤销函数

};

其中的undo()函数实现的就是撤销我们execute()执行的功能。下面就是我们前面移动命令的撤销函数实现:

class MoveUnitCommand : public Command

{

public:

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

  : unit_(unit),

    xBefore_(0),

    yBefore_(0),

    x_(x),

    y_(y)

  {}

 

  virtual void execute()

  {

    // Remember the unit's position before the move

    // so we can restore it.

    xBefore_ = unit_->x();

    yBefore_ = unit_->y();

 

    unit_->moveTo(x_, y_);

  }

 

  virtual void undo()

  {

    unit_->moveTo(xBefore_, yBefore_);

  }

private:

  Unit* unit_;

  int xBefore_, yBefore_;

  int x_, y_;

};

这里我们添加了几个新的变量。因为当我们移动一个角色的时候,会遗忘到我们之前移动的位置,现在加上xBefore_,yBefore来保存单位移动钱的位置。

为了能让玩家撤销之前的操作,我们需要保留玩家执行过的命令。当玩家需要撤销的时候,执行命令的undo操作即可。当然如果玩家又想重做,我们再执行一次命令的execute()即可。

想要支持多层的撤销也很简单,只要我们有一个命令队列来记录玩家的命令。然后在通过指向当前玩家的命令块,来做想要的操作即可。

 

当玩家选择”undo”,我们就调用当前命令的undo,然后,让队列的当前指针指向前一个命令。当玩家选择”Redo”,我们就让对了的当前指针向后移动一个,再执行其execute即可。如果玩家在撤销后,执行了新的命令,那么在当前位置插入新的命令,同时删除掉后面的所有命令(这个大家可以在word里面测试下,就是这个规则哈)。

我第一次使用这个模式是在关卡编辑器里面,感觉自己就像是一个天才。我吃惊于它如此的简单直接,但是工作的确如此的完美。虽然它需要,确保你的每一个编辑的操作都用命令封装一次,但是一旦完成后,剩下的工作就都顺其自然的完成了。

翻译就到这里,在原贴http://gameprogrammingpatterns.com/command.html里面还有一些其他的注意事项和提示,大家可以去看看。

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值