在使用 Unity 開發遊戲的時候,為了實現各種功能,往往會不斷衍伸出一個又一個的系統,分別執掌不同的任務,可能是為了管理 UI 介面,也可能是為了建立連線,又或者是為了管理存檔。一個個的系統往往又為了方便而採用 Singleton pattern,或者互相注入,最後的結果就是系統之間的關係複雜,程式碼不易維護及重複使用。
於是乎我就一直思考著如何將一個巨大的系統架構,拆分成一個個獨立且靈活的小系統,就像電腦與周邊設備可以用 USB 輕易連接與斷開,我也希望我所開發的一個個系統,可以自由自在地安置在不同的開發專案之中。
如今這是我的初步成果,一套用於拆分以及管理各個子系統的架構:
上面的圖片展示了再開始設計這套架構時,所期望達到的幾個特性 (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; 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(); } } 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(); } 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 如果加入了其他管理器或功能,則實現了眾管理器的統一介面。
特別提出設計模式並不是要表達如何透過設計模式去解決問題,而是為了規劃出具有相當維護性的架構,我不知不覺中會聯想到我曾經讀過的模式,進而設計出自己獨有的模式,而不是直接取用書上的方法。
希望大家也能在規劃程式時有各種體會,不只為了解決問題,同時也能享受在其中。