二、命令模式
2.1 命令模式的总结与介绍
将一个请求封装成一个对象,从而允许使用不同的请求、队列或日志将客户端参数化,同时支持请求操作的撤销与恢复。
命令就是一个对象化(实例化)的方调用、面向对象化的回调。
2.1.1 命令模式的结构
- Command: 抽象命令类
- MoveCommand: 具体命令类
- Invoker: 调用者
- Actor: 接收者
- Client:客户类
2.1.1 命令模式的分析
- 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
- 让命令作为一个类,来进行调用,而不是命令仅仅只是一段可执行逻辑代码。
- 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。(实现动态绑定)
- 命令作为一个对象以后就可以和其他对象一样进行保存、传递、转移等等。
白话的讲一下就是按下按键A,执行移动命令。而移动命令是一段代码,命令模式就是声明一个命令接口,把这段命令代码封装在一个类里面,然后继承自命令接口,然后声明个接口的指针,按照需要把这个指针指向new出来移动类(或者其他命令类),然后我们把按键A和这个指针绑定,如果按键A被按下了,就代表发出了这个请求(我要移动!),然后执行命令接口的excute指令就行了。
2.1.2 命令模式的实现
class Command
{
public:
virtual void execute() = 0;
};
class MoveCommand : public Command
{
public:
MoveCommand(Actor* p) : pActor_(p) {}
virtual void execute() { pActor_->Action(); };
Actor* pActor_;
};
class Actor
{
public:
void Action() {};
};
class Invoker
{
public:
Invoker(Command* pCommand) : pCommand_(pCommand) {}
void Invoke() { pCommand_->execute(); }
private:
Command* pCommand_;
};
int main()
{
Actor* pActor = new Actor();
Command* pCommand = new MoveCommand(pActor);
Invoker* pInvoker = new Invoker(pCommand);
pInvoker->Invoke();
// ...释放内存或者做其他事情
}
2.1.3 特点
- 优点:
保存命令队列
容易实现撤销重做
加一个新的命令不影响其他类
把请求一个操作的对象与执行一个操作的对象分隔开 - 缺点:
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
2.2 命令模式游戏中的应用
- 举个例子:
一个游戏的操作运行,是需要一个外部输入设备输入命令的,比如鼠标、键盘。假设有两个按键A,B,按键A可以让人物前进,按键B可以让人物跳起来,那人物前进的过程就是:用户按下A键->游戏接收到A被按下的信息->找到人物前进的代码->执行人物前进。
那上面的例子就可以简单写个代码
void Input::InputCmd()
{
if(PressedBtn(BUTTON_A))
move();
else if(PressedBtn(BUTTON_B))
jump();
// ···
}
那么问题来了,我想改一下按键,想按A跳起来,按B前进,应该怎么写代码,不可能再复制粘贴一遍上面的代码只是把BUTTON_A,BUTTON_B交换一下。。
那么就可以把命令封装成一个类,继承自命令基类,然后用基类的指针保存不同的按键命令。
class Command
{
public:
virtual ~Command() {}
virtual void execute() = 0;
};
class JumpCommand : public Command
{
public:
virtual void execute() { jump(); }
};
class MoveCommand : public Command
{
public:
virtual void execute() { move(); }
};
输入处理的代码
class Input
{
public:
void InputCmd()
{
if(PressedBtn(BUTTON_A)) buttonA_->execute();
else if(PressedBtn(BUTTON_B)) buttonB_->execute();
//...
}
// 指针绑定不同的命令对象,如果想换绑之类的直接把指针对象指向另一个命令对象就行
void bindButton()
{
buttonA_ = new JumpCommand();
buttonB_ = new MoveCommand();
}
private:
Command* buttonA_;
Command* buttonB_;
};
那如果要操控不同对象进行移动或者跳跃呢。比如服务器下发屏幕里另一个玩家在进行移动,那我本地电脑应该也能看到另一个玩家在移动的。肯定是本地电脑在执行另一个玩家移动的操作。
那我们的命令类里可以传入一个对象指针或引用,指定对象执行对应的命令
class Command
{
public:
virtual ~Command() {}
virtual void execute(GameActor& actor) = 0;
};
class JumpCommand : public Command
{
public:
virtual void execute(GameActor& actor)
{
actor.jump();
}
};
如果从服务器下发一个其他玩家移动的命令,为了让我本地看到其他玩家移动,就需要获取命令和对应的actor
Command* command = getcmd(); // 可能从服务器下发或者玩家输入的命令
if(command)
{
command->execute(actor);
}
撤销与重做
撤销命令这个行为在策略游戏中经常见到,在游戏中可以回滚一些不满意的步骤。
而且比如说一些编辑软件里都会有撤销命令Ctrl+Z,游戏中的UI编辑器或者关卡编辑器里也会经常用到。
而且在游戏回放录像的时候,简单的实现方法就是记录每一帧所执行的命令,然后从头跑一遍记录的所有命令就能看到游戏的回放了。
为了可以撤销命令,我们可以保存一个命令列表和一个对当前命令的引用
- 当执行一个命令的时候,把命令添加到列表里面,并将current指向它
- 如果是撤销命令,我们需要把当前的命令回退过去,然后当前current指向列表前一个;
- 如果是重做命令,那么把当前列表的current指向下一个命令,重新执行。
- 如果回退以后重新进行一个命令,那么把当前列表的当前命令之后所有命令都舍弃(也可以把重做也当成一个命令保存)