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

Compasslg 發表於 2021-04-23

演算法、資料結構、與設計模式等在遊戲開發中的運用 (一):單例設計(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上也有一些不小的專案用到過單例設計;同時,我自己在遊戲開發過程中也常常感受到這種設計模式帶來的便利。所以,我覺得只要合理運用,單例設計也不失為一種好的設計模式。如有不同意見或者高手有什麼指教,歡迎評論。