@TOC
命令模式(Command)
GoF的描述生涩难懂
命令模式是我最喜欢的模式之一。在我编写的绝大多数的程序、游戏或者其他东西中,最终在某些地方都会使用到它。当我在合适的地方使用它,它能把一些棘手的代码清晰地整理解开。对于这样出色的模式,GoF对此有个让人生涩难懂的描述:
将请求封装为对象,从而使用户能够以不同的请求对客户端进行参数化、对请求进行排队或记录,并支持可撤销的操作。
这是个很糟糕的描述。首先,这个描述歪曲了想要表达的含义。特别是"客户端"这个词,抛开软件领域,“客户端"可以代表一个人,比如一个与你做生意的人。据我所知,人类不能被"参数化”。
然后,这个描述的剩余部分只是你可以/可能使用该模式的概念清单。除非你用例存在概念恰好在上述清单中,否则没有太大启发意义。
命令是具体化的方法调用
"具体化"表示将某个概念转化为一段数据(一个对象:你可以将其放入变量、传递给函数等等)。因此,通过说命令模式是“具体化的方法调用”,我的意思是它是一个包装在对象中的方法调用。
取决于你使用的编程语言和习惯,具体而言这听起来很像一个“回调函数”、“一等函数”、“函数指针”、“闭包”或“部分应用函数”等概念,而且它们确实都在同一个范围内。GoF后来表示:
命令是一种替代回调的面向对象方案
配置输入映射
在每个游戏程序中,都有一段代码读取原始的用户输入,比如按键、键盘输入、鼠标点击等事件。这段代码检查每个事件同时转译为对应的动作,如下举例所示:
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();
}
这个函数在游戏循环的每帧中被调用。如果我们固定了用户输入与游戏动作的映射,那么这段代码是可行的。但是绝大多数游戏是允许用户配置输入映射的。
为了支持输入映射可配置的操作,那么意味着上述的中每个条件分支下的对应行为是可变的(举例:当满足isPressed(BUTTON_X)这个条件后,转译的动作是可变的。)。"可变的"在软件中往往暗示着一个变量,变量是可变的,因此我们可以用一个变量去代表游戏动作,我们可以用个抽象的基类代表一个游戏动作, 如下所示:
class Command
{
public:
virtual ~Command() {}
virtual void execute() = 0; //execute是个子类必须实现的虚函数,用来执行最终的动作
};
那么我们可以创建对应不同游戏动作的子类实现:
class JumpCommand : public Command
{
public:
virtual void execute() { jump(); }
};
class FireCommand : public Command
{
public:
virtual void execute() { fireGun(); }
};
// You get the idea...
在InputHandler中,为了进行配置输入映射,变为了如下所示的代码结构:
class InputHandler
{
public:
// 使用函数指针执行动作
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();
}
// 增加输入映射配置的功能
void reMapX(Command* action)
{
this->buttonX_ = action;// 对X进行配置
};
void reMapY(Command* action)
{
this->buttonY_ = action;// 对Y进行配置
};
// 为了A和B等按键映射...
// Methods to bind commands...
private:
Command* buttonX_; //函数指针
Command* buttonY_;
Command* buttonA_;
Command* buttonB_;
};
引入角色
上述的实现动作的函数,比如jump()、fireGun()等,这些函数是无参函数,说明只能对固定的对象执行对应的固定动作。如果引入角色,那么可想而知,玩家角色和Npc角色的jump、fireGun等动作肯定是有所区别的。因此Command基类将引入传参actor:
class Command
{
public:
virtual ~Command() {}
virtual void execute(GameActor& actor) = 0;
};
动作的实现函数也将发现变化:
class JumpCommand : public Command
{
public:
virtual void execute(GameActor& actor)
{
actor.jump(); // jump函数的最终实现被绑定到了actor上
}
};
因此,handleInput()函数也可以发生改变,返回对应的命令实例对象,如下所示
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_;
// 未实现,返回NULL
return NULL;
}
因为handleInput函数没有传入具体的角色对象,所以上述代码没有立刻执行实际的命令。这带来一个优点:我们可以延后进行命令的执行。如下所示:
Command* command = inputHandler.handleInput();
if (command)
{
command->execute(actor);
}
我们所举的这个例子中,actor代表被用户输入驱动的玩家角色。我们通过Command对象实现了决策和动作的分离,因此我们自然也可以想到游戏中的AI角色(Npc),如果被控角色是一样的,那么AI角色跟玩家角色的不同之处就是驱动命令的来源问题,因此我们可以用下述的示意图统一表示这种模式
💡 批注:为什么特意使得命令可序列化,成为流呢?我们可以把命令流通过网络传输。因此我们能够把玩家输入通过网络传输到另外的机器上,并且重现它。这在多人网络游戏中是很重要的功能。

最低0.47元/天 解锁文章
1282

被折叠的 条评论
为什么被折叠?



