游戏开发设计模式:命令模式

二、命令模式

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指向下一个命令,重新执行。
  • 如果回退以后重新进行一个命令,那么把当前列表的当前命令之后所有命令都舍弃(也可以把重做也当成一个命令保存)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值