2016年5月15日 星期日

策略模式 - Strategy Pattern

彈幕射擊遊戲是一種結合了「射擊」和「閃避」的遊戲類別,在敵人放出的大量子彈(彈幕)空隙間閃避。我們就以製作一款彈幕射擊遊戲為目標,來探索各類的設計模式的技巧吧!



遊戲想法

我們想要設計一個類似「雷電」的縱向捲軸彈幕射擊遊戲。我們先從敵人的物件開始下手吧!稍為介紹一下遊戲中會出現的敵人類型


  • 不同種類的敵人會有不一樣的移動方式,有的可能直線前進,有的會走 S 型,甚至有的敵人衝著玩家而來。
  • 特殊種類的敵人可能會發射子彈攻擊玩家。而發射子彈也有不同的方式,有的發射直線子彈,有的子彈朝玩家飛來。
  • 每一架敵人飛機都會有被擊中時的「爆炸」事件。



看到這裡,設計師通常會有以下的設計概念:
  • 因為所有的敵人都有一樣的擊中爆炸的功能,所以我們創建一個超類別(super-class)來處理這個部分,也就是上圖中「Enemy」的 Hit()。
  • 每一個敵人應該都有移動方式攻擊。所以讓次形態(subtype)繼承自超類別後,在負責實踐自己的移動行為攻擊行為,也就是上圖的 PlaneA ~ C。
對於有學過繼承概念的設計師來說,這樣的方法還蠻直覺的。但這樣的設計其實有一些淺在的問題。
  • PlaneA 和 PlaneC 的攻擊都是直線子彈,這意謂著我需要寫兩支一模一樣的 Function 在這兩個次型態裡面。S型前進的功能也是如此。
  • 遊戲編導想要新增一個新的敵人飛行機 PlaneD,它會快速的移動,但他竟然不會攻擊(沒有人規定敵人一定要會攻擊對吧!)。看來我只能幫他寫個 Attack() 但裡面是空的。這樣真的好嗎?如果遊戲編導又要新增一個敵人砲台,它會發射散型子彈攻擊,但不會移動。我又要留個空白的Move()?
  • 遊戲編導想要增加掉落道具的功能 DropItem(),但不是每一種敵人飛行機都會掉,我到底要寫在超類別還是次形態裡好呢?

使用繼承出現的問題
  • 改變超類別會影響到所有的子類別,但有些子類別卻不適用。
  • 有的子類別會有重複的功能,重寫程式碼顯然不是個好方法。


設計守則

  1. 找出程式中可能會變動的地方,把他們獨立出來,不要和固定不變的程式混在一起。
  2. 針對介面而寫,而不是針對實踐方式而寫。不要為了實踐超類別而被綁的死死的。
  3. 多用點合成(Composition),少用繼承

程式中可能會變動的地方指的是什麼呢?在這個例子中,移動行為攻擊行為掉落道具會依不同角色而改變,這就是會變動的部分。將它們分開寫成三個介面。




我們將移動、攻擊掉落道具的行為交給別人來處理,而不是定義在角色類別中的方法。那我們該如何整合敵人「Enemy」類別和行為呢?首先,我們先處理超類別的部分。



在這裡,Enemy 只要呼叫 performAttack 就可以了,至於要怎麼攻擊,我們交由 atk_behavior 物件來幫忙處理。完全不用在乎 AttackBehavior 介面的物件是什麼。

再來,我們來關心子類別們。這裡我們直接用簡略的程式碼來展示最後一個步驟
public class PlaneA extends Enemy{
    public PlaneA(){
        atk_behavior = new AttackForward();
        mov_behavior = new MoveForward();
    }

    // More...
}

在設計守則的第二點「針對介面而寫,而不是針對實踐方式而寫」。這句話可能有點抽象,但他的技巧就在於「多型」的概念。針對介面而寫,其實指的就是「針對超類別而寫」。再說明確一點,我們在宣告變數時,應該要是超型態 ( 通常會是抽象類別或一個介面 )。像上面這段程式碼,atk_behavior 這個變數其實在 Enemy 中是用 AttackBehavior 宣告而成的。但在實踐時,卻是用 AttackForward 這個次形態。因此,我們在撰寫 Enemy 這一層類別時,宣告的變數類別,根本不用理會他執行時真正的物件型態。

所以說,在這裡我們用「多型」的概念。當某變數的實際型態(actual type)形式型態 (formal type)不一致時,呼叫此變數的方法 (也就是 Enemy中 performAttack 裡做的事情),一定會呼叫到「正確」的版本, 就是實際型態的版本。也就是說,PlaneA 繼承自 Enemy,Enemy中有 performAttack ,performAttack 中我們呼叫了 attackBehavior 的 attack。而這個 attack 到底是要做怎樣的攻擊?我們便在 PlaneA 類別中的建構子來定義它( atk_behavior = new AttackForward(); )。所以,它事實上是去執行了 AttackForward 中的直線攻擊動作。


經過修改後,新增移動和掉落物品的行為並不會影響到其他的角色了。而且,某些功能也可以被其他物件再三利用。這就是所謂的策略模式 (Strategy Pattern)



參考資料 : 

Head First Design Pattern 深入淺出 設計模式

沒有留言:

張貼留言