C#小遊戲,分析三種設計模式:單例模式,策略模式和工廠方法模式

遊戲簡介

簡介


遊戲是一個非常簡單的RPG小遊戲,遊戲中主要有兩類角色,分別爲Hero-英雄和Enemy-敵人(怪獸),英雄是由玩家控制的角色,怪獸是系統控制的角色,其中怪獸分爲不同等級,有小怪和大怪,遊戲內容比較簡單,就是雙方發射子彈攻擊對方,如果怪獸將英雄的生命值打爲0,遊戲結束,如果英雄將最後的大怪生命值打爲0,遊戲勝利。

 

界面演示


本來想做成GIF動畫演示的,但是由於GIF文件比較大,上傳不了,這裏貼張圖片,展示一下游戲的界面,大家可以下載源碼運行,就可以看到整個遊戲運行過程了。
操作說明:"X"鍵發射子彈,方向鍵控制人物的移動

遊戲整體結構


打開VS工程,打開其中的類圖文件ClassDiagram1.cd,就可以看到整個遊戲的類圖了

VS工程

遊戲的類圖如下

簡單分析一下游戲的結構

Element:所有角色的根類
RoAndMi:繼承自Element,是角色和子彈的基類
Roles及其子類:遊戲中的所有角色
Missiles及其子類:遊戲中所有角色的子彈
FireBehavior及其子類:遊戲中所有角色的發射子彈的行爲
HitCheck:遊戲的主控類,用來控制遊戲中所有元素
遊戲詳細的實現過程,讀者可以看源碼,結合類圖看源碼,相信讀者很快就能非常清楚整個遊戲了
下面的三個部分是遊戲的核心

Roles及其子類:遊戲中的所有角色
FireBehavior及其子類:遊戲中所有角色的發射子彈的行爲
HitCheck:遊戲的主控類,用來控制遊戲中所有元素

分析遊戲的時候,要把握好這三塊。
下面我們就結合這個小遊戲,分析三種設計模式:單例模式策略模式工廠方法模式

 

單例模式


定義


確保一個類只有一個實例,並提供一個全局訪問點。[1]P177(表示在參考文獻[1]的177頁,下同)

 

經典的單例模式實現


public class Singleton {
    private static Singleton uniqueInstance;
 
    // other useful instance variables here
 
    private Singleton() {}
 
    public static Singleton GetInstance() 
    {
        if (uniqueInstance == null) 
        {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
 
    // other useful methods here
}

 


總結一下單例的實現就是:一個私有,兩個靜態

一個私有
就是私有構造函數
單例模式的思想就是一個類只有一個實例,即外部任何類都不能實例化該類,那麼什麼樣的類外部不能實例化呢?我們知道,實例化一個類的時候,需要調用構造函數,而一般構造函數都是public的,所以能夠被外部調用,所以能夠在外部實例化,當將構造函數設置爲private時,外部就不能調用類的構造函數了,也就不能實例化該類了,該類只能在類的內部實例化。這個思想是實現單例模式的關鍵。
兩個靜態
1.靜態成員變量uniqueInstance,該成員變量就是類的唯一實例
2.靜態方法GetInstance(),用來獲取該類的唯一實例

前面提到了使用私有構造函數是實現單例模式的關鍵,那麼下面的問題就是怎麼在外部獲取該單例呢?由於任何外部類都不能實例化該類,所以我們無法通過使用new一個類的對象來調用類裏面的方法獲取單例 ( 即不能通過

Singleton singleton = new Singleton();

singleton.GetInstance()

來獲取單例 ),只能通過類裏面的靜態方法,通過類名調用靜態方法

Singleton.GetInstance() 

)來獲取單例,而靜態方法只能調用靜態成員,所以類的成員變量也必須是靜態的。

 

適用性


當一個類只能有一個實例而且客戶可以從一個衆所周知的訪問點訪問它時。[2]P84

對有些類來說,只有一個實例很重要,如線程池,註冊表,文件系統
雖然全局變量也可以提供全局訪問點,但是不能防止你實例化多個對象

 

遊戲中的實現


類圖文件中雙擊HitCheck類,就能看到代碼,當然也可以在工程中直接打開HitCheck.cs

    /// <summary>
    /// 主控類,負責遊戲中的各種角色的管理
    /// 1.AddElement()---添加元素
    /// 2.RemoveElement()----刪除元素
    /// 3.Draw()----元素的繪製
    /// 4.DoHitCheck()---元素之間的碰撞檢測
    /// 5.Restart()---重新開始遊戲
    /// </summary>
    public class HitCheck
    {
        //遊戲中的角色
        private Hero myHero = null;
        private List<MissileHero> missileHero = new List<MissileHero>();
        private List<Roles> enemy = new List<Roles>();
        private List<Missiles> enemyMissile = new List<Missiles>();
        private List<Element> bombs = new List<Element>();
        /// <summary>
        /// 構造函數私有化,禁止在其他地方實例化
        /// </summary>
        private HitCheck() { }

        private static  HitCheck instance;

        public static HitCheck GetInstance()
        {
            if (instance == null)
            {
                instance = new HitCheck();
            }
            return instance;
        }
        ...
   }


這個代碼看上去是不是很熟悉,這就是個典型的單例模式的實現:一個私有,兩個靜態.

 

爲什麼要使用單例模式: Manager管理器


剛開始寫遊戲的時候是沒有用的,慢慢發現,遊戲中的角色一旦過多,角色就很難管理,如角色的產生,角色的死亡,包括角色之間的碰撞檢測。一旦遊戲中要增加角色需要修改的代碼很多,維護量比較大,所以就想設計一個類,實現對遊戲中所有角色的管理,這樣就可以很方便的對遊戲中的角色進行管理。這個類主要控制遊戲中的所有角色,包括對所有元素的增加,刪除,以及碰撞檢測(如英雄是否被敵人的子彈打中),這就要求該類只能有一個實例,不能有多個實例。不然遊戲就會出錯,所以設計爲單例。讀者分析一下HitCheck的源碼就非常清楚其中使用單例的原因了。

 

 

多線程問題


經過上面的介紹和分析,讀者對基本單例模式的實現和原理應該比較清楚了,那麼是否這樣的單例模式就非常好了呢?下面我們討論一下在多線程中的問題。
還是看上面經典單例模式的代碼

public class Singleton {
    private static Singleton uniqueInstance;
 
    // other useful instance variables here
 
    private Singleton() {}
 
    public static Singleton GetInstance() 
    {
        if (uniqueInstance == null) 
        {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
 
    // other useful methods here
}
 
假設現在有兩個線程,以下是他們的執行步驟:

多線程中,由於每個線程執行的順序不確定,就有可能產生2個實例。
那怎麼解決呢?這裏提供以下兩種方式,有更好的方式,歡迎大家提出來。

 

方法1:”急切”實例化


public class Singleton {
    private static Singleton uniqueInstance = new Singleton();
 
    private Singleton() {}
 
    public static Singleton GetInstance() {
        return uniqueInstance;
    }
}
 
代碼中,當類被加載時,靜態變量uniqueInstance 會被初始化,此時類的私有構造函數會被調用,單例類的唯一實例將被創建。多線程的時候,由於類加載的時候就創建了實例,所以不會出現多個實例的情況。

 

方法2:“雙重檢查加鎖”


class Singleton 

    private static volatile Singleton instance = null; 
    //程序運行時創建一個靜態只讀的輔助對象
    private static readonly object syncObject= new object();

    private Singleton() { } 

    public static Singleton GetInstance() 
    { 
        //第一重判斷,先判斷實例是否存在,不存在再加鎖處理
        if (instance == null) 
        {
            //臨界區!
            //加鎖的程序在某一時刻只允許一個線程訪問
            lock(syncObject)
            {
                //第二重判斷
                if(instance==null)
                {
                    instance = new Singleton();  //創建單例實例
                }
            }
        }
        return instance; 
    }
}

 
爲了更好地對單例對象的創建進行控制,此處使用了一種被稱之爲雙重檢查加鎖機制。在雙重檢查鎖定中,當實例不存在且同時有兩個線程調用GetInstance()方法時,它們都可以通過第一重instancenull判斷,然後由於lock鎖定機制,只有一個線程進入lock中執行創建代碼,另一個線程處於排隊等待狀態,必須等待第一個線程執行完畢後纔可以進入lock鎖定的代碼,如果此時不進行第二重instancenull判斷,第二個線程並不知道實例已經創建,將繼續創建新的實例,還是會產生多個單例對象,因此需要進行雙重檢查。

volatile關鍵字
volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。當成員變量發生變化時,強迫線程將變化值回寫到共享內存(線程共享進程的內存)。這樣,讀取這個變量的值時候每次都是從momery裏面讀取而不是從cache讀,這樣做是爲了保證讀取該變量的信息都是最新的,而無論其他線程如何更新這個變量。
此外,由於使用volatile關鍵字屏蔽掉了一些必要的代碼優化,所以在效率上比較低,因此需要慎重使用。
如果沒有volatile關鍵字,第二個線程就可能沒有及時讀到最新的值,比如進程2在第二重判斷的時候,進程1已經產生了一個實例,但是進程2沒有讀到最新的值,讀到的instance還是爲null,那麼就會產生多個實例了,那麼即使使用了雙重檢查加鎖,也有可能產生多個實例。

這兩種方式在[1]P180~P182的處理多線程問題中也有非常清楚的闡述,用java描述,深入淺出,講解地非常好。

 

兩種方式的比較


”急切”實例化在類被加載時就將自己實例化,它的優點在於無須考慮多個線程同時訪問的問題,可以確保實例的唯一性;從調用速度和反應時間角度來講,由於單例對象一開始就得以創建,因此要優於“雙重檢查加鎖”。但是無論系統在運行時是否需要使用該單例對象,由於在類加載時該對象就需要創建,因此從資源利用效率角度來講,”急切”實例化單例不及“雙重檢查加鎖”單例,而且在系統加載時由於需要創建”急切”實例化單例對象,加載時間可能會比較長。

“雙重檢查加鎖”單例類在第一次使用時創建,無須一直佔用系統資源,實現了延遲加載,但是必須處理好多個線程同時訪問的問題。

 

結束語


遊戲中還有兩個模式:策略模式和工廠方法模式在下面的博客中講述,讀者可以先看看下載下來的資料中的PPT的相關內容,結合類圖,可以先自行分析遊戲源碼中的這兩個模式。第一次使用Markdown寫博客,雖然不太熟練,但是覺得Markdown還是很強大的,寫出的博客更美觀。

這裏順便推薦大家一本書:《Head First設計模式》,該本書獲2005年第15屆Jolt大獎,Jolt大獎是軟件行業的"奧斯卡"獎。本書中的每個設計模式都結合具體實例,深入淺出,個人覺得比GOF設計模式更加通俗易懂。
      
該書的所有代碼我上傳到CSDN上了,結合書中的代碼看這本書會更好。

 


引用:https://blog.csdn.net/qianqing13579/article/details/45511721

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章