重返设计模式--命令模式

news/2024/11/17 22:43:15/

理论要点

  • 什么是命令模式:“将一个请求封装成一个对象,从而允许你使用不同的请求、队列或日志将客服端参数化,同时支持请求操作的撤销与恢复。” 通俗讲就是把方法的调用封装进对象中去回调

  • 优缺点:对类间解耦、可扩展性强、易于命令的组合维护、易于与其他模式结合,而缺点是会导致类的膨胀。

  • 使用场景:
    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可以灵活前后多层回档。其实也很简单:我们不单单记录最后一条指令,还要记录指令列表,然后用一个引用指向“当前”的那个。 当玩家执行一条命令,我们将其添加到列表,然后将代表“当前”的指针指向它。
这里写图片描述
当玩家选择“撤销”,我们撤销现在的命令,将代表当前的指针往后退。 当他们选择“重做”,我们将代表当前的指针往前进,执行该指令。 如果在撤销后选择了新命令,那么清除命令列表中当前的指针所指命令之后的全部命令。

嗯,就这样了,结束~~


http://www.ppmy.cn/news/258204.html

相关文章

Silverlight游戏设计(Game Design):(六)场景编辑器之开源畅想

所有的游戏设计辅助工具都是为了提高游戏开发效率而出现的,Silverlight-2D游戏场景编辑器(QXSceneEditor)同样也不例外,虽然它不比《魔兽》、《星际》、《帝国》等大作的地图编辑器拥有强大到甚至可以通过“换肤”直接创造出一款新游戏;但是大…

三国群英传服务器端架设修改,三国群英传OL单机架设视频教程

简单文字说明: 先还原数据库 修改Account 下的 AccountServer.ini sql_ip 127.0.0.1 sql_port 3306 sql_account sa 数据库用户名 sql_password 123456 密码 // sql_inout_ip 127.0.0.1 sql_inout_port 3306 sql_inout_account sa数据库用户名 sql_inout_pas…

三国群英传服务器端架设修改,【三国OL单机假设】三国群英传架设单机方法

【三国OL单机假设】三国群英传架设单机方法 (2013-08-14 16:47:48) 标签: 游戏 近期有很多网友加我QQ,问我架设三国群英传ol9005版的单机方法,在此做一详述,就不再接受一个一个地询问了。 同时,要申明的是,本人只是一个三国群英传爱好者,所有的架设理论都是来自游戏藏宝…

三国群雄传ol服务器 修改,三国群英传OL DATA.PAK相关修改

三国群英传OL DATA.PAK相关修改 本文出处:网游动力作者:本站发布时间:2009-08-24阅读次数: 一修改小怪的经验:打开Players.txt exp = 6 将这里的数字提高100倍就是100倍经验 二修改怪物的爆率:打开DropItem.txt 对照Players.h里的怪物ID 修改爆率,比如: drop = 1,item…

三国群英传2修改MOD基础

三国群英传2的MOD制作,必须修改的几个ini文件: SANGO.INI——武将的武器、马匹、物品 THINGS.INI——战场中的对象:兵种、兵种在战场的设定、武器等 TIMES1-4.INI——剧本1、2、3、4。武将的属性设置。 MAGIC.INI——武将技、军师技 上面文件…

【unity细节】Default constructor not found for type Player(找不到默认构造函数)

👨‍💻个人主页:元宇宙-秩沅 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 本文由 秩沅 原创 收录于专栏:unity细节和bug ⭐Default constructor not found for type Player ⭐ 文章目录 ⭐Default const…

第五章、用户体验五要素之结构层解析(本文作用是通俗讲解,让你更容易理解)

范围层确定之后,我们要把零散的、概括性的需求细化整理成一条一条的业务线,然后组成一个整体,这就是结构层。 在结构层,功能型产品关注交互设计,就是那些影响用户执行和完成任务的元素。信息型产品关注信息结构…

TAG页和站内搜索页要注意的问题

百度网页搜索反作弊团队近期发现一部分网站遍历热门关键词生成大量的站内搜索结果页来获取搜索引擎流量,其中存在大量的不相关内容严重损害了搜索引擎的用户体验并且侵占了相应领域的优质网站收益,对于此类网站我们将做出严厉的处理,希望存在…