[Unity] 應用 Singleton pattern 及 Unity Component 做系統拆分與管理 – Dividing your game system in unity

在使用 Unity 開發遊戲的時候,為了實現各種功能,往往會不斷衍伸出一個又一個的系統,分別執掌不同的任務,可能是為了管理 UI 介面,也可能是為了建立連線,又或者是為了管理存檔。一個個的系統往往又為了方便而採用 Singleton pattern,或者互相注入,最後的結果就是系統之間的關係複雜,程式碼不易維護及重複使用。

於是乎我就一直思考著如何將一個巨大的系統架構,拆分成一個個獨立且靈活的小系統,就像電腦與周邊設備可以用 USB 輕易連接與斷開,我也希望我所開發的一個個系統,可以自由自在地安置在不同的開發專案之中。

如今這是我的初步成果,一套用於拆分以及管理各個子系統的架構:

game system manager

上面的圖片展示了再開始設計這套架構時,所期望達到的幾個特性 (Feature):

  • 於 Unity 專案程式 (Application) 啟動當下,可以自動初始化已經採用、不定數量的子系統。
  • 程式執行中途,可以隨時添加或拆卸子系統。
  • 程式執行中途,可以在任意時機地點輕易呼叫到特定子系統,有不輸給使用 Singleton pattern 的便利性。

而這些特性我也一一在架構中實現了,使用的方案接下來依序介紹。

子系統基底 – IGameSystemMono

首先準備子系統基礎類別,用來統一所有子系統的基本控制接口:

1
2
3
4
5
6
7
8
9
using UnityEngine;
using System.Collections;

namespace DouduckGame {
public abstract class IGameSystemMono : MonoBehaviour {
public abstract void StartGameSystem();
public abstract void DestoryGameSystem();
}
}

在 IGameSystemMono 類別的設計中,首先繼承了 MonoBehaviour 這個 Unity component 的基礎類別,如此一來可以得到兩個特性:

  • 可以在 Unity Inspector 上面看見子系統的 public 參數,在不修改程式碼的情況下進行序列化參數的調整。
  • 即使沒有使用子系統管理的架構,每個子系統也可以做為單純的 Component 應用於專案之中。

以上這兩個特性可以讓子系統的使用更加靈活,減少改寫程式碼的需求;但反過來也有限制,那就是整個子系統管理的架構必須依賴著一個 DontDestoryObject 作為載體才能運作。

一個掛載的子系統操作參數的方式跟 Component 相當接近

另外,這個IGameSystemMono類別中定義了兩個 abstract method 函式,用來給予子系統管理器主動呼叫,分別用於取代 Start() 及 OnDestory() 這兩個原生於 MonoBehaviour 的函式。這樣的設計是為了實現隨時 添加或拆卸子系統 的特性,避免 Start() 及 OnDestory() 沒有在預期的時機生效的錯誤,可以由管理器來決定呼叫的時機。

不過雖然設計上要避免使用 Start() 及 OnDestory() 兩個 message (已經用 abstract method 函式取代),但是 Update() 等其他 message 還是可以使用。

子系統管理器 – GameSystemManager

接下來是整個系統的核心,用來管理與控制的管理器類別:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

namespace DouduckGame {
public sealed class GameSystemManager {

private bool m_bIsInitialized = false;

private GameObject m_oContainer;
private Dictionary<Type, IGameSystemMono> m_GameSystemList;

// Initializaion method
public GameSystemManager(GameObject oContainer) {
m_oContainer = oContainer;
GameObject.DontDestroyOnLoad(m_oContainer);

m_GameSystemList = new Dictionary<Type, IGameSystemMono> ();
}

public void StartInitialSystem() {
if (m_bIsInitialized) {
return;
}
m_bIsInitialized = true;

IGameSystemMono[] systemList_ = m_oContainer.GetComponents<IGameSystemMono>();
for (int i = 0; i < systemList_.Length; i++) {
m_GameSystemList.Add(systemList_ [i].GetType(), systemList_ [i]);
systemList_ [i].StartGameSystem();
}
}

// Functional method
public void AddSystem<T> () where T : IGameSystemMono {
if (m_GameSystemList.ContainsKey(typeof(T))) {
Debug.LogError("[GameSystemManager] There was a " + typeof(T).Name);
} else {
T gameSys_ = m_oContainer.AddComponent<T> ();
gameSys_.StartGameSystem ();
m_GameSystemList.Add(gameSys_.GetType(), gameSys_);
}
}

public void RemoveSystem<T> () where T : IGameSystemMono {
if (m_GameSystemList.ContainsKey(typeof(T))) {
IGameSystemMono gameSys_ = m_GameSystemList [typeof(T)];
m_GameSystemList.Remove(typeof(T));
gameSys_.DestoryGameSystem();
GameObject.Destroy(gameSys_);
} else {
Debug.LogError("[GameSystemManager] There was no " + typeof(T).Name);
}
}

public void EnableSystem<T> () where T : IGameSystemMono {
if (m_GameSystemList.ContainsKey(typeof(T))) {
m_GameSystemList [typeof(T)].enabled = true;
} else {
Debug.LogError ("[GameSystemManager] There was no " + typeof(T).Name);
}
}

public void DisableSystem<T> () where T : IGameSystemMono {
if (m_GameSystemList.ContainsKey(typeof(T))) {
m_GameSystemList [typeof(T)].enabled = false;
} else {
Debug.LogError ("[GameSystemManager] There was no " + typeof(T).Name);
}
}

public T GetSystem<T> () where T : IGameSystemMono {
if (m_GameSystemList.ContainsKey(typeof(T))) {
return m_GameSystemList [typeof(T)] as T;
} else {
Debug.LogError ("[GameSystemManager] There was no " + typeof(T).Name);
return null;
}
}
}
}

這個類別並沒有採用 MonoBehaviour 做為基底,一方面並沒有使用相關功能的必要性;另一方面是希望將類別的功能單純化,讓類別功能專注於子系統的管理,至於如何啟動與呼叫這個管理器將在下一段進行說明。

子系統管理器除了常態的建構子,需要傳入一個 gameobject 作為子系統的載體外 (因為子系統繼承了 MonoBehaviour,需要有物件掛載),還有一個 public method 函式 StartInitialSystem() 可以將原本就已經在物件上的子系統進行初始化,將用於實現在專案程式啟動時自動初始化已掛載子系統的特性需求。

另外為了實現隨時添加或拆卸子系統特性,準備了四個 public method 函式:

  • AddSystem() – 增加一個子系統,同時會呼叫子系統的 StartSystem()
  • RemoveSystem() – 卸除一個子系統,同時會呼叫子系統的 DestorySystem()
  • EnableSystem() – 將子系統的 MonoBehaviour.enabled 設為 true
  • DisableSystem() – 將子系統的 MonoBehaviour.enabled 設為 false

很明顯可以發現,這些函式都使用了泛型,而整個管理器用了一個 Dictionary 來儲存所有的子系統。如此設計的最大好處就是,整個管理器在使用上就跟 getComponent() 等 Unity 原生 API 相似及直觀,不需要額外的 id 或 string 來做為呼叫子系統的 key。

而最後的 GetSystem 函式,應該不需特別說明,便是取得掛載的子系統之方法。

將管理器包裝成一個簡單呼叫的工具 – DouduckGameCore

最後只剩下容易呼叫這個特性還沒實現了,原本在開發時我會盡量避免使用 Singleton pattern,以免專案會越來越難維護。不過現在我們有了一個子系統的管理器,這時候即使採用 Singleton pattern,那未來專案也不會持續增加更多的子系統 Singleton,因為所有需要被呼叫的遊戲系統,接統一在這個管理器之下了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using UnityEngine;
using UnityEngine.Events;
using System.Collections;

namespace DouduckGame {
public sealed class DouduckGameCore : MonoBehaviour {
public static GameObject InstanceGameObject;
private static GameSystemManager m_SystemManager;
private static bool m_bIsInitialized = false;

private void Awake () {
if (m_bIsInitialized) {
Debug.LogError("[DouduckGameCore] was initialized");
Object.Destroy(this);
} else {
m_bIsInitialized = true;
transform.name = "[DouduckGameCore]";
GameObject.DontDestroyOnLoad(this.gameObject);
InstanceGameObject = this.gameObject;
m_SystemManager = new GameSystemManager (InstanceGameObject);
}
}

void Start () {
m_SystemManager.StartInitialSystem();
}

// *** System manager method ***
public static void AddSystem<T> () where T : IGameSystemMono {
m_SystemManager.AddSystem<T>();
}

public static void RemoveSystem<T> () where T : IGameSystemMono {
m_SystemManager.RemoveSystem<T>();
}

public static void EnableSystem<T> () where T : IGameSystemMono {
m_SystemManager.EnableSystem<T>();
}

public static void DisableSystem<T> () where T : IGameSystemMono {
m_SystemManager.DisableSystem<T>();
}

public static T GetSystem<T> () where T : IGameSystemMono {
return m_SystemManager.GetSystem<T>();
}
}
}

最後 DouduckGameCore 這個腳本的使用方法,便是在場景中建立一個空物件,並將 DouduckGameCore 與希望一開始就啟動的子系統全掛載於其上即可。

這段腳本簡單來說只做了兩件事:

  • 將 GameSystemManager 重新包裝成 Singleton MonoBehaviour,來達成專案中隨時隨地都可以呼叫的特性。
  • 在 Awake() 的地方建立 GameSystemManager,並在 Start () 時呼叫 StartInitialSystem() 這個函式,將同樣掛載在這個物件底下的子系統進行初始化,完成專案程式啟動時自動初始化已掛載子系統的特性。

接下來在專案中任何地方需要子系統時,只要一段程式碼即可呼叫:

1
DouduckGameCore.GetSystem<MyGameSystem>();

後話

目前這個子系統管理的架構還有一些改善的空間,但就目前來說,使用起來的感受已經相當愉快,載開發專案的過程減少了許多打亂程式碼的疑慮。另外就是看到自己的程式碼保留了相當程度的可動性,這件事情本身就帶來了不少成就感。

如果要說這個架構中使用了甚麼設計模式,除了很明顯的 Singleton pattern 外,就是採用了某種程度上的 Facade pattern 的理念,在 GameSystemManager 實現了子系統的統一取得介面,而 DouduckGameCore 如果加入了其他管理器或功能,則實現了眾管理器的統一介面。

特別提出設計模式並不是要表達如何透過設計模式去解決問題,而是為了規劃出具有相當維護性的架構,我不知不覺中會聯想到我曾經讀過的模式,進而設計出自己獨有的模式,而不是直接取用書上的方法。

希望大家也能在規劃程式時有各種體會,不只為了解決問題,同時也能享受在其中。