遊戲編程模式--命令模式

寫在前面

  最近深感代碼設計對於軟件開發過程當中的重要性,因此從新拾起了設計模式,之前學的比較鬆散,理解不夠,這一次本着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#,果斷推薦使用函數來定義命令。

結語

  顯而易見,命令模式簡單理解就是命令面向對象化的回調。把一個個行爲、請求封裝爲一個一個命令對象,使得命令的生產和命令的調用解耦,避免硬編碼的壞味道。

相關文章
相關標籤/搜索