重返设计模式--命令模式
理論要點
什么是命令模式:“將一個請求封裝成一個對象,從而允許你使用不同的請求、隊列或日志將客服端參數化,同時支持請求操作的撤銷與恢復。” 通俗講就是把方法的調用封裝進對象中去回調。
優缺點:對類間解耦、可擴展性強、易于命令的組合維護、易于與其他模式結合,而缺點是會導致類的膨脹。
使用場景:
1,命令模式很適合實現諸如撤消,重做,回放,時間倒流之類的功能。
2,基于命令模式實現錄像與回放等功能,也就是執行并解析一系列經過預錄制的序列化后的各玩家操作的有序命令集合(如游戲AI,Demo演示等)。
代碼分析
1, 我們每個游戲都會有處理玩家輸入事件,它記錄每次的輸入,并將之轉換為游戲中一個有意義的動作,如下圖:
通常我們會這樣直觀的實現:
好,我們來分析下上面的代碼,想想通常我們游戲的按鍵是可以自定義設置的,那么如果玩家要自定義按鍵,就不得不改這段代碼的邏輯了,調用函數交換來交換去,是不是覺得很麻煩而且容易出錯~
2,下面我們再來這樣改裝下:
首先,我們定義一個基類,代表可觸發的游戲動作命令:
然后,我們為每個具體的游戲動作創建子類:
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 JumpCommand : public Command { public:virtual void execute(GameActor& actor){actor.jump();} };...現在,我們可以使用這個類讓游戲中的任何角色跳來跳去了。在這之前我們還缺了點代碼,就是讓生成命令與執行命令分開,解耦了生產者和消費者:
首先是生成命令:
然后,需要一些接受命令的代碼,作用在玩家角色上。
Command* command = inputHandler.handleInput(); if(command) {command->execute(actor); }這樣我們就可以控制游戲中的任何角色,只需向命令傳入不同的角色。試想下,這個在我們游戲中可以運用在哪里?
沒錯,角色AI,我們是不是可以這樣,讓AI代碼負責生成命令,游戲角色執行命令。這樣在選擇命令的AI和展現命令的游戲角色間解耦給了我們很大的靈活度。我們可以把生成命令放入隊列中,即命令流的形式去執行:
一些代碼(輸入控制器或者AI)產生一系列命令放入流中。 另一些代碼(調度器或者角色自身)調用并消耗命令。 通過在中間加入隊列,我們解耦了消費者和生產者。
4,最后的這個例子是這種模式最廣為人知的使用情況。 如果一個命令對象可以做一件事,那么它亦可以撤銷這件事。 這在策略游戲中經常使用,如《三國群英傳III》,我們走過的格子還可以撤銷回來。
下面我們先來實現一個簡單的移動命令:
然后是處理接口:
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 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可以靈活前后多層回檔。其實也很簡單:我們不單單記錄最后一條指令,還要記錄指令列表,然后用一個引用指向“當前”的那個。 當玩家執行一條命令,我們將其添加到列表,然后將代表“當前”的指針指向它。
當玩家選擇“撤銷”,我們撤銷現在的命令,將代表當前的指針往后退。 當他們選擇“重做”,我們將代表當前的指針往前進,執行該指令。 如果在撤銷后選擇了新命令,那么清除命令列表中當前的指針所指命令之后的全部命令。
嗯,就這樣了,結束~~
總結
以上是生活随笔為你收集整理的重返设计模式--命令模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: halcon中阈值分割算子用法
- 下一篇: asp.net ajax控件工具集 Au