定义
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();
}
但是这样写不够优雅,而且修改命令的话还要修改调用类,此时如果我们想要解耦的话就可以使用命令模式。
抽象出规则:
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();
}
以前每个输入直接调用函数,现在会有一层间接寻址:
角色说明
但是这种写法只能操作主角,让我们放松这个限制, 不让函数去找它们控制的角色,我们将函数控制的角色对象传进去。
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_, y_;
};
输入控制代码可以在玩家下决定时创造一个实例。就像这样:
Command* handleInput()
{
Unit* unit = getSelectedUnit();
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),
xBefore_(0),
yBefore_(0),
x_(x),
y_(y)
{}
virtual void execute()
{
// 保存移动之前的位置
// 这样之后可以复原。
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_的作用。
支持多重的撤销也不太难。 我们不单单记录最后一条指令,还要记录指令列表,然后用一个引用指向“当前”的那个。 当玩家执行一条命令,我们将其添加到列表,然后将代表“当前”的指针指向它。
当他们选择“重做”,我们将代表当前的指针往前进,执行该指令。 如果在撤销后选择了新命令,那么清除命令列表中当前的指针所指命令之后的全部命令。
命令模式的优点
1.在命令模式中, 请求者不直接与接收者交互, 即请求者不包含接收者的引用, 因此彻底消除了彼此之间的耦合
2.命令模式满足"开-闭"原则. 如果增加新的具体命令和该命令的接收者, 不必修改调用者的代码, 调用者就可以使用新的命令对象; 反之, 如果增加新的调用者, 不必修改现有的具体命令和接收者, 新增加的调用者就可以使用自己已有的具体命令
3.由于请求者被封装到了具体命令中, 那么就可以将具体命令保存到持久化的媒介中, 在需要的时候, 重新执行这个具体命令. 因此使用命令模式可以记录日志
4.使用命令模式可以对请求者的"请求"进行排队. 每个请求都各自对应一个具体命令,因此可以按照一定的顺序执行这些命令