演算法、數據結構、與設計模式等在遊戲開發中的運用 (一):單例設計(Singleton Design)

演算法、數據結構、與設計模式等在遊戲開發中的運用 (一):單例設計(Singleton Design)

作者: Compasslg 李涵威

1. 什麼是單例設計(Singleton Design)

在學校學習面向對象編程中的一些常用的設計模式時,我第一次系統的接觸到了單例設計(Singleton Design),或者說單例設計模式。所謂設計模式(Design Pattern),指的是在軟體開發中針對一些常見問題提出的可復用的解決方式;而單例設計便是針對在面向對象編程中一些只會被實例化一次、或只允許一個實例(instance)存在的類(class)而出現的設計模式。這種對象/實例通常是作為工程中一個全局管理的存在,因此他們會在整個工程的各個角落被調用。由於在面向對象編程的設計中你往往需要通過儲存一個對象的地址來隨時調用其中的方法和數據,這便可能會造成同一個對象的地址被儲存很多次的情況。
在單例設計模式中,你可以通過將單例目標類的構造器(Constructor)設置為private類型使該類無法在外部被實例化,然後在這個類的內部實例一個自己的對象並將其儲存在一個靜態變數中。同時,你也可以寫一個公開的靜態getter方法來作為獲取和調用這個單例的方式。如此這般,通過使用單例設計模式,你便可以實現一個全局有且只有一個實例的類。具體實現方式我會在下一個部分舉例說明。

2. 如何使用單例設計 (Java 範例)

Java是單例設計最常被應用的語言。一個最典型的例子便是java.lang.Runtime中的Runtime class. 該類無法被實例,你可以通過其中的靜態方法Runtime.getRuntime()來調用他的單例 object。

除此之外,在軟體和遊戲開發中還有很多的功能可以利用單例設計實現,以下便是一個用Java通過單例設計實現的可能被應用在遊戲和軟體中的音頻管理器(AudioManager)的簡單模型:

/**===========================================
	這是一個在遊戲中管理音頻文件的類,有且只有存在一個。
	因此使用單例設計模式。
  *===========================================*/
  class AudioManager {
      // 設置為private,外部便不能再修改這個單例。
      private static instance;
      
      // 單例中實際儲存的內容。這裡使用的數據類型只是作為該單例的應用背景(context),僅供參考,在AudioManager中具體要用什麼數據類型來儲存音頻資料應視情況而定
      private HashMap<String, AudioClip> clips;
      
      // ... 此處略過其他AudioManager可能需要的變數 ...
      
      // private 構造器,無法被外部調用
      private AudioManager(){
          clips = new HashMap<String, AudioClip>();
          // ... 此處略過其他可能需要初始化的東西
      }
      
      // 單例的 Getter。會且只會實例一個該類的實例。
      // 如果擔心第一次調用時的速度影響,可以在loading的時候統一將單例設計的類的getInstance方法調用一次
      public static AudioManager getInstance(){
          // 實例化如果此前沒有實例
          if (instance == null){
              instance = new AudioManager();
          }
          return instance;
      }
      
      // 播放一段音頻的方法
      public void playAudioClip(String clipName){
          clips.get(clipName).play();
      }
  }

以下是調用方法:

public static void main(String[] args){
	AudioManager.getInstance().playAudioClip("BGM");
}

3. 遊戲開發中的運用 (Unity)

在遊戲開發過程中,我們常常會需要應用到一些負責全局管理的並且只會同時存在一個實例的類,例如上面提到的AudioManager,還有負責管理遊戲狀態或介面的StateManager和SceneManager,我自己寫遊戲也經常會寫一個負責數據管理的類DataManager。這些類都是有且只有一個實例存在,都可以通過 Part 2 中的方法依樣畫葫蘆實現和調用,這裡就不複述了。

在使用Unity開發遊戲時,我們往往會要用到一個GameController。在我接觸單例設計之前,我都會選擇在要用到他的地方存一個變數,然後通過在編輯器中把它拖到inspector,或者使用

gameController = GameObject.FindWithTag("GameController");

來找到它。久而久之,這樣不但使程式碼變得混亂且重複,在速度上和記憶體空間上也給我一種很浪費的感覺。這個時候,我們可以利用Singleton Design思想,在GameController中加一個靜態變數

public class GameController : MonoBehaviour {
 
	private static GameController instance;
	void Awake(){
		// ... 省略其他初始化相關程式碼 ...
		instance = this;
	}

	public static GameController GetInstance(){
		return instance;
	}
}

這樣,雖然這個類是綁定Unity中的GameObject被實例的而並非按照此前提到的通過private constructor的方法生成的單例,我們依然可以利用單例設計中的部分思想來使他調用起來更方便。此後,當我們需要調用GameController中的方法時,只需要調用他的單例即可

GameController.GetInstance().MethodName();

4. 總結

在我學習OOP中常用的設計模式的過程中,我的教授表達了並不推薦學生使用單例設計的看法。他認為單例設計 「破例的使用了全局變數,破壞了模組化設計的思想 」。但此後,我在學習過程中多次看到其他教授使用單例設計,Github上也有一些不小的項目用到過單例設計;同時,我自己在遊戲開發過程中也常常感受到這種設計模式帶來的便利。所以,我覺得只要合理運用,單例設計也不失為一種好的設計模式。如有不同意見或者高手有什麼指教,歡迎評論。