游戏编程模式-命令模式(Command)

@TOC

命令模式(Command)

GoF的描述生涩难懂

命令模式是我最喜欢的模式之一。在我编写的绝大多数的程序、游戏或者其他东西中,最终在某些地方都会使用到它。当我在合适的地方使用它,它能把一些棘手的代码清晰地整理解开。对于这样出色的模式,GoF对此有个让人生涩难懂的描述:

将请求封装为对象,从而使用户能够以不同的请求对客户端进行参数化、对请求进行排队或记录,并支持可撤销的操作。

这是个很糟糕的描述。首先,这个描述歪曲了想要表达的含义。特别是"客户端"这个词,抛开软件领域,“客户端"可以代表一个人,比如一个与你做生意的人。据我所知,人类不能被"参数化”。

然后,这个描述的剩余部分只是你可以/可能使用该模式的概念清单。除非你用例存在概念恰好在上述清单中,否则没有太大启发意义。

命令是具体化的方法调用

"具体化"表示将某个概念转化为一段数据(一个对象:你可以将其放入变量、传递给函数等等)。因此,通过说命令模式是“具体化的方法调用”,我的意思是它是一个包装在对象中的方法调用。

取决于你使用的编程语言和习惯,具体而言这听起来很像一个“回调函数”、“一等函数”、“函数指针”、“闭包”或“部分应用函数”等概念,而且它们确实都在同一个范围内。GoF后来表示:

命令是一种替代回调的面向对象方案

配置输入映射

在每个游戏程序中,都有一段代码读取原始的用户输入,比如按键、键盘输入、鼠标点击等事件。这段代码检查每个事件同时转译为对应的动作,如下举例所示:

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();
}

这个函数在游戏循环的每帧中被调用。如果我们固定了用户输入与游戏动作的映射,那么这段代码是可行的。但是绝大多数游戏是允许用户配置输入映射的。

为了支持输入映射可配置的操作,那么意味着上述的中每个条件分支下的对应行为是可变的(举例:当满足isPressed(BUTTON_X)这个条件后,转译的动作是可变的。)。"可变的"在软件中往往暗示着一个变量,变量是可变的,因此我们可以用一个变量去代表游戏动作,我们可以用个抽象的基类代表一个游戏动作, 如下所示:

class Command
{
public:
  virtual ~Command() {}
  virtual void execute() = 0; //execute是个子类必须实现的虚函数,用来执行最终的动作
};

那么我们可以创建对应不同游戏动作的子类实现:

class JumpCommand : public Command
{
public:
  virtual void execute() { jump(); }
};

class FireCommand : public Command
{
public:
  virtual void execute() { fireGun(); }
};

// You get the idea...

在InputHandler中,为了进行配置输入映射,变为了如下所示的代码结构:

class InputHandler
{
public:
    // 使用函数指针执行动作
    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();
    }
    // 增加输入映射配置的功能
    void reMapX(Command* action)
    {
        this->buttonX_ = action;// 对X进行配置
    };
    void reMapY(Command* action)
    {
        this->buttonY_ = action;// 对Y进行配置
    };
    // 为了A和B等按键映射...
  // Methods to bind commands...

private:
    Command* buttonX_; //函数指针
    Command* buttonY_;
    Command* buttonA_;
    Command* buttonB_;
};

引入角色

上述的实现动作的函数,比如jump()、fireGun()等,这些函数是无参函数,说明只能对固定的对象执行对应的固定动作。如果引入角色,那么可想而知,玩家角色和Npc角色的jump、fireGun等动作肯定是有所区别的。因此Command基类将引入传参actor:

class Command
{
public:
  virtual ~Command() {}
  virtual void execute(GameActor& actor) = 0;
};

动作的实现函数也将发现变化:

class JumpCommand : public Command
{
public:
  virtual void execute(GameActor& actor)
  {
    actor.jump(); // jump函数的最终实现被绑定到了actor上
  }
};

因此,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_;
  // 未实现,返回NULL
  return NULL;
}

因为handleInput函数没有传入具体的角色对象,所以上述代码没有立刻执行实际的命令。这带来一个优点:我们可以延后进行命令的执行。如下所示:

Command* command = inputHandler.handleInput();
if (command)
{
  command->execute(actor);
}

我们所举的这个例子中,actor代表被用户输入驱动的玩家角色。我们通过Command对象实现了决策和动作的分离,因此我们自然也可以想到游戏中的AI角色(Npc),如果被控角色是一样的,那么AI角色跟玩家角色的不同之处就是驱动命令的来源问题,因此我们可以用下述的示意图统一表示这种模式

💡 批注:为什么特意使得命令可序列化,成为流呢?我们可以把命令流通过网络传输。因此我们能够把玩家输入通过网络传输到另外的机器上,并且重现它。这在多人网络游戏中是很重要的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值