游戏编程模式--命令模式

写在前面

  最近深感代码设计对于软件开发过程当中的重要性,因此从新拾起了设计模式,之前学的比较松散,理解不够,这一次本着learning,try,Teaching的精神,从新认识和学习设计模式。这一次参考Robert Nystrom 著的《游戏编程模式》一书,与原先的GoF所著的24种设计模式不一样,但思想是相通的,读者如果想对本文的设计模式追根溯源,可自行购买参照。c++

命令模式

  GoF这样表述命令模式:将一个请求(request)封装成一个对象,从而容许你使用不一样的请求、队列或日志将客户端参数化,同时支持请求操做的撤销和恢复。编程

  其实GoF还有一个更简单的描述:命令就是面向对象化的回调。c#

  对于这两种描述,相信读者一开始都会以为比较的抽象,咱们接下来将会举例说明命令模式的应用场景。设计模式

配置输入

  设想咱们早期的游戏机,咱们使用手柄做为输入,手柄上有几个按键,好比“A”,“B”,“C”等,每当咱们按下其中一个按键时,游戏中的角色就会作相应的一个动做。若是咱们要实现这个过程,相信咱们很容易写出这样的实现代码:闭包

  

void InputHandler::handlInput() { if(isPressed(BUTTON_A)) { jump(); } else if(isPressed(BUTTON_B)) { fire(); } else   //do otherthing 
 { } }

   这种方式是能够运行的,也能够达到咱们的目的,但很明显,这种硬编码的风格很是的不灵活,并且若是咱们想对按钮和其映射的行为进行配置的话是无能为力的。这个时候咱们就可使用命令模式了。ide

  在命令模式中,咱们首先定义一个基类来表明命令:函数

class Command { public: virtual ~Command() {} virtual void Excute() = 0; };

  而后为不一样的命令创建子类:学习

class JumpCommand : public Command { public: virtual void Excute() override { std::cout << "jump" << std::endl; } }; class FireCommand : public Command { public: virtual void Excute() override { std::cout << "fire" << std::endl; } };

  在输入处理类中为每个按键存储一个命令指针,而后输入处理便经过这些指针进行代理:编码

class InputHandler { public: InputHandler() { } void HandleInput() { if(isPressed(BUTTON_A)) { button_a->excute(); } else if(isPressed(BUTTON_B)) { button_b->excute(); } else { } } private: Command* button_a; Command* button_b; }; 

  完成这写步骤以后,代码还不能马上执行,还须要为InputHandler的左右按键配置相应的命令。spa

    

inputHandler.setButtonCommand(BUTTON_A,new JumpCammand);

   这样经过为每输入的处理添加一个间接调用实现了按键与命令的解耦,极大的方便了后续关于按键处理的修改。这就是命令模式,它的有点是显而易见的。

  但在上述的例子中咱们并无判断命令为空的状况,事实上咱们能够定义一个空命令,这个命令不作任何的事情,每个按键的默认命令就是空命令,这即是空值对象模式,这种模式在不少状况下能够简化咱们的代码逻辑。

  除此以外,这个例子还有一点不足。一般在游戏中有不少的角色,相同的类型角色均可以执行相同的命令(这种状况可能没有想象中的那么广泛),那在InputHandler如何分辨那个角色执行命令了?咱们能够把角色传入命令中,而后命令使用这个角色来执行对应的指令,好比:

class FireCommand : public Command { public: virtual void Excute(GameActor& actor) override { actor.Fire(); } };
Cammand* InputHandler::handleInput()
{
    if (isPress(BUTTON_A))
        return buttonA_;
    if (isPress(BUTTON_B))
        return buttonB_;

    return nullptr;
}
Cammand* cmd = inputHandler.handleInput(); if (cmd != nullptr) { cmd->excute(actor); }

  除了上述的应用场景,咱们还能够考虑另外一个应用场景——AI。在游戏中,咱们一般会有很是多的非玩家控制的角色,这些角色的行为都是由AI系统控制的,若是都是用硬编码的形式来编写,最后的代码会给你带来地狱般的体验。这个使用,若是使用命令模式将带来极大的便利性。例如AI系统想构建一个具备侵略性的敌人,那只须要在AI系统中插入一段生成侵略性指令的代码便可。AI系统负责生产命令,而命令的执行则由目标角色调用。再进一步的思考,命令产生后,角色须要顺序执行命令,那就须要一个队列来存储未执行的命令,这种状况就比如一个命令流,经过命令流咱们就是实现了命令生产端和消费端的解耦。

 重作和撤销

  命令模式还有另外一个经常使用的场景——撤销和重作。现代社会基本的编辑类应用都会提供这样的操做(想象一下你在编辑一个文档,不当心按下删除按钮整段内容删除却不能撤销这个操做的状况将是多么可怕),使用命令模式会很是方便的实现这个功能。修改一下以前的命令类,添加撤销和重作的方法。

class Command { public: ~Command() {} virtual void Excute(GameActor& actor) = 0; virtual void Undo() = 0;  //重作 };

  以后再维护一个已执行命令和已撤销命令的栈就能轻松实现撤销和重作功能。

类风格化仍是函数风格化

  在这里,咱们使用了类来定义命令,主要鉴于c++中闭包支持有限(c++11中闭包须要手动管理内存,比较麻烦)。其实命令模式从某些方面看来是某些没有闭包的语言模拟闭包的一个方式。在支持闭包的语言中,如JS,c#,果断推荐使用函数来定义命令。

结语

  显而易见,命令模式简单理解就是命令面向对象化的回调。把一个个行为、请求封装为一个一个命令对象,使得命令的生产和命令的调用解耦,避免硬编码的坏味道。

相关文章
相关标签/搜索