命令模式笔记

将一个请求封装成一个对象,从而允许你使用不同的请求、队列或日志将客户端参数化,同时支持请求操作的销毁与恢复。

配置输入

每个游戏都有一处代码块用来读取用户原始输入:按钮点击、键盘事件、鼠标点击,或者其他输入等。

它记录每次的输入,并将之转换为游戏中一个有意义的动作。

简单实现:

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

上面代码将用户的输入硬编码到游戏的行为中去,为了支持自定义配置,定义一个基类来代表一个可触发的游戏命令:

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

为每个按钮存储一个指向它的指针:

class InputHandler
{
public:
    void handleInput();
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() 等这样的顶级函数能够隐式地获知玩家游戏实体并对其进行木偶般的控制。

传进去一个想要控制的对象而不是让命令自身来确定所控制的对象:

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_;

      return NULL;
}

接收命令并让象征着玩家的角色执行命令:

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

命令和角色之间加入的间接层使得可以让玩家控制游戏中的任何角色,只需通过改变命令执行时传入的角色对象即可。

将控制角色的命令作为头等对象便解除了函数直接调用这样的紧耦合。

输入处理或 AI 生成命令并将它们置于命令流中,发送者或角色自身执行命令并且调用它们。

通过中间队列将生产者和消费者端解耦。

撤销和重做

移动一个单位:

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_;
    int y_;
}

每次玩家选择一个动作,输入处理程序都会创建一个命令实例:

Command * handleInput()
{
    Unit *unit = getSelectUnit();
    if (isPressed(BUTTON_UP))
    {
        int destY = unit->y() - 1;
        return new MoveUnitCommand(unit, unit->x(), destY);
    }
    if (isPressed(BUTTON_DOWN))
    {
        int destY = unit->y() + 1;
        return new MoveUnitCommand(unit, unit->x(), destY);
    }
    //其他移动操作
    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), x_(x), y_(y), xBefore_(0), yBefore(0) {}

    virtual void execute()
    {
        //移动之前记录unit的位置
        xBefore_ = unit_->x();
        yBefore_ = unit_->y();
        unit_->moveTo(x_, y_);
    }

    virtual void undo()
    {
        unit_->moveTo(xBefore_, yBefore_);
    }

private:
    Unit* unit_;
    int x_, y_;
    int xBefore_, yBefore_;
}

维护一个命令列表和一个对“当前”命令的一个引用来支持多次撤销。

当玩家执行了一个命令,将这个命令添加到列表中,并将“current”指向它。

当选择“撤销”时,撤销当前命令并且将当前指针移回去。

当选择“重做”时,将指针前移然后执行它所指向的命令。

如果撤销之后选择了一个新命令,列表中位于当前命令之后的所有命令都被舍弃掉。

类风格化还是函数风格化

命令模式对于没有闭包的语言来说是模拟闭包的一种方式。

用函数式风格实现命令模式:

JavaScript 创建一个单位移动命令:

function makeMoveUnitCommand(unit,x,y)
{
    return function(){
        unit.moveTo(x,y);
    }
}

通过闭包来添加对撤销的支持:

function makeMoveUnitCommand(unit,x,y)
{
    var xBofore,yBefore;
    return {
        execute:function()
        {
            xBofore = unit.x();
            yBefore = unit.y();
            unit.moveTo(x,y);
        },
        undo:function()
        {
            unit.moveTo(xBofore,yBefore);
        }
    };
}

参考

posted @ 2020-07-18 22:11  CodeWithMe  阅读(145)  评论(0)    收藏  举报