撰写了文章 更新于 2017-12-29 21:50:37
游戏编程模式学习笔记 第二章 命令模式
将一个请求(request)封装成一个对象,从而允许你使用不同的请求、队列或日志将客户端参数化,同时支持请求操作的撤销与修复。
命令就是一个对象化(实例化)的方法调用(A command is reified method call)。
“reified”一词,意即“具象化”(make real)。另一个术语reifying的意思是使一些事物成为“第一类”(first-class)。
这两个术语都意味着,将某个概念(concept)转化为一块数据(data)、一个对象,或者你认为是传入函数的变量等。所以说命令模式是一个“对象化的方法调用”,我的意思就是封装在一个对象中的一个方法调用。
GoF后面这样补充到:
命令就是面向对象化的回调(Commands are an object-oriented replacement for callbacks)。
参考一个游戏里面使用命令控制角色的代码,且控制的命令是可配置的。
class Command
{
public:
virtual ~Command() {}
virtual void execute(GameActor& actor) = 0;
};
class JumpCommand : public Command
{
public:
virtual void execute(GameActor& actor)
{
actor.jump();
}
};
class InputHandler
{
public:
void handleInput();
// Methods to bind commands...
private:
Command* buttonX_;
Command* buttonY_;
Command* buttonA_;
Command* buttonB_;
};
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_;
// Nothing pressed, so do nothing
return NULL;
}
//然后我们需要一些代码接受命令并让象征着玩家的角色执行命令。
Command * command = inputHandler.handleInput();
if(command)
{
command->execute(actor);
}
我们可以照搬上面的命令模式来作为AI引擎和角色之间的接口;AI代码简单地提供命令(Command)对象以供执行。
选择命令的AI和表现玩家的代码之间的解耦为我们提供了很大的灵活性。我们可以对不同的角色使用不同的AI模块。我们可以针对不同种类的行为将AI进行混搭。
人工智能-> 命令流 -> 角色
一些代码(输入处理或者AI)生成命令并将他们放置于命令流中,一些代码(发送者或者角色自身)执行命令并且调用它们。通过中间的队列,我们将生产者端和消费者端解耦。
撤销和重做
一次性命令的特质很快能被我们所用。为了使命令变得可撤销,我们定义了一个操作,每个命令类都需要来实现它:
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), x_(x), y_(y), xBefore_(0), yBefore_(0)
{}
virtual void execute()
{
// Remember the unit's position before the move
// so we can restore it.
xBefore_ = unit_->x();
yBofore_ = unit_->y();
unit_->moveTo(x_, y_);
}
virtual void undo()
{
unit_->moveTo(xBefore_, yBefore_);
}
private:
Unit* unit_;
int x_, y_;
int xBefore_, yBefore_;
};
为了让玩家能够撤销一次移动,我们保留了他们执行的上一个命令。当他们敲击Control_Z时,我们便会调用改命令的undo()方法。
支持撤销多次撤销并不难。这次我们不再保存最后一个命令,取而代之的是,我们维护一个命令列表喝一个对“当前”命令的一个引用。当玩家执行了一个命令,我们将这个命令添加到列表中,并将"current"指向它。
当玩家选择“撤销”时,我们撤销当前的命令并且将当前的指针移回去。当他们选择“重做”时,我们将指针前移然后执行它所指向的命令。如果他们在撤销之后选择了一个新的命令,那么列表中位于当前命令之后的所有命令都被舍弃掉。
类风格化还是函数风格化
在有GC管理的语言里面,可以考虑使用函数式的风格实现命令模式,在某些方面,命令模式对于没有闭包的语言来说是模拟闭包的一种方式。
function makeMoveUnitCommand(unit, x, y){
return function() {
unit.moveTo(x,y);
}
}
我们也可以通过闭包来添加对撤销的支持:
function makeMoveUnitCommand(unit, x, y){
var xBefore, yBefore;
return {
execute: function() {
xBefore = unit.x();
yBefore = unit.y();
unit.moveTo(x, y);
},
undo : function() {
unit.moveTo(xBefore, yBefore);
}
};
}
参考
1、你可能最终会有很多不同的命令类。为了更容易地实现这些类,可以定义一个具体的基类,里面有着一些实用的高层次的方法,这样便可以通过对派生出来的命令组合来定义其行为,这么做通常是有帮助的。它会将命令的主要方法execute()变成子类沙盒。
2、在我们的例子中,我们明确地选择了那些会执行命令的角色。在某些情况下,尤其是在对象模拟分层的情况下,它可能没这么直观。一个对象可以响应一个命令,而它也可以决定将命令下放给其从属对象。如果你这样做,你需要了解下责任链(Chain of Responsibility)。
3、一些命令如第一个例子中的JumpCommand是无状态的的纯行为的代码块。在类似这样的情况下,拥有不止一个这样命令类的实例会浪费内存,因为所有的实例是等价的。享元模式就是解决这个问题的。
风潇然 [作者] 1年前
Yoge 1年前
风潇然 [作者] 1年前
发布