理论要点
什么是命令模式:“将一个请求封装成一个对象,从而允许你使用不同的请求、队列或日志将客服端参数化,同时支持请求操作的撤销与恢复。” 通俗讲就是把方法的调用封装进对象中去回调。
优缺点:对类间解耦、可扩展性强、易于命令的组合维护、易于与其他模式结合,而缺点是会导致类的膨胀。
使用场景:
1,命令模式很适合实现诸如撤消,重做,回放,时间倒流之类的功能。
2,基于命令模式实现录像与回放等功能,也就是执行并解析一系列经过预录制的序列化后的各玩家操作的有序命令集合(如游戏AI,Demo演示等)。
代码分析
1, 我们每个游戏都会有处理玩家输入事件,它记录每次的输入,并将之转换为游戏中一个有意义的动作,如下图:
通常我们会这样直观的实现:
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();
}
好,我们来分析下上面的代码,想想通常我们游戏的按键是可以自定义设置的,那么如果玩家要自定义按键,就不得不改这段代码的逻辑了,调用函数交换来交换去,是不是觉得很麻烦而且容易出错~
2,下面我们再来这样改装下:
首先,我们定义一个基类,代表可触发的游戏动作命令:
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();
}
恩,到目前为止,命令模式其实就这样悄悄出现了,以前每个输入直接调用一个函数,而现在则是增加了一个间接调用层对象,简而言之,这就是命令模式。
3,注意到没,上面代码还有这样一个问题:如JumpCommand对象执行的行为是直接作用到主角身上的,即它只能让主角跳跃。
下面我们来优化下,不让函数去找它们控制的角色,我们将函数控制的角色对象传进去:
class Command
{
public:virtual ~Command() {}virtual void execute(GameActor& actor) = 0;
};
相应的子类:
class JumpCommand : public Command
{
public:virtual void execute(GameActor& actor){actor.jump();}
};...
现在,我们可以使用这个类让游戏中的任何角色跳来跳去了。在这之前我们还缺了点代码,就是让生成命令与执行命令分开,解耦了生产者和消费者:
首先是生成命令:
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,我们是不是可以这样,让AI代码负责生成命令,游戏角色执行命令。这样在选择命令的AI和展现命令的游戏角色间解耦给了我们很大的灵活度。我们可以把生成命令放入队列中,即命令流的形式去执行:
一些代码(输入控制器或者AI)产生一系列命令放入流中。 另一些代码(调度器或者角色自身)调用并消耗命令。 通过在中间加入队列,我们解耦了消费者和生产者。
4,最后的这个例子是这种模式最广为人知的使用情况。 如果一个命令对象可以做一件事,那么它亦可以撤销这件事。 这在策略游戏中经常使用,如《三国群英传III》,我们走过的格子还可以撤销回来。
下面我们先来实现一个简单的移动命令:
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* hanleInput()
{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; //撤销命令
};
然后来修改子类中的实现:
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;
};
对于命令模式,这种撤销操作是不是很简单就实现了。
最后,真的是最后了~来科普下怎么实现多重撤销,就像我们IDE那样按ctrl + z和ctrl + y可以灵活前后多层回档。其实也很简单:我们不单单记录最后一条指令,还要记录指令列表,然后用一个引用指向“当前”的那个。 当玩家执行一条命令,我们将其添加到列表,然后将代表“当前”的指针指向它。
当玩家选择“撤销”,我们撤销现在的命令,将代表当前的指针往后退。 当他们选择“重做”,我们将代表当前的指针往前进,执行该指令。 如果在撤销后选择了新命令,那么清除命令列表中当前的指针所指命令之后的全部命令。
嗯,就这样了,结束~~