[C#/Unity] 更多 Singleton – More Singleton in Unity

在舊文章 [Unity] 應用 Singleton pattern 及 Unity Component 做系統拆分與管理 – Dividing your game system in unity 中,我開發了一套架構來作為單一 Singleton 的替代方案,用於統一管理會在整個 Unity 專案中使用到的遊戲功能系統。

該篇文章中的 GameSystemMono (即仿 Singlton 的組件) 繼承了 MonoBehaviour,來實現一些設計上的想法。不過帶來優點的同時也產生了一些限制,經過與他人的討論後,認為還是需要一個不依賴 MonoBehaviour 的 Singleton 組件方案,兩者互相補足,而這個想法終於在最近進行了實作。

做為 GameModule 基底的介面 (interface)

上一個 Singleton 系統將組件命名 GameSystemMono;而這次的組件我則命名為 GameModule,下文也會如此稱呼與區分。

而 GameModule 的採用介面 (interface) 來實現,其中 IModular 是必備的,IModuleUpdatable 是有 Update 需求時可以增加的介面。目前只開發了兩個介面,未來如有需要,還能繼續擴充。

1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DouduckGame {
public interface IModular {
void ModuleInitialize ();
void ModuleUninitialize ();
}
public interface IModuleUpdatable {
void ModuleUpdate ();
}
}

採用**介面(interface)而非抽象類別(abstract class)**的原因:

  • 因為並沒有繼承 MonoBehaviour,因此並沒有寫為類別的需要。
  • C# 不支援多重繼承 (Multiple Inheritance),所以如果採用類別,則會降低組件繼承上的靈活性。
  • 介面可以支援多重組合,而且是可以在繼承之外另外加上的,相當於可以將多種類別都轉換成 GameModule 來使用而不受限。

GameModule 的管理器

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
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;

namespace DouduckGame {
public class GameModuleManager {

private Dictionary<Type, IModular> m_GameModuleList;
private List<IModuleUpdatable> m_GameModuleUpdateList;

public GameModuleManager () {
m_GameModuleList = new Dictionary<Type, IModular> ();
m_GameModuleUpdateList = new List<IModuleUpdatable> ();
}

public void UpdateModule () {
for (int i = m_GameModuleUpdateList.Count - 1; i >= 0; i--) {
m_GameModuleUpdateList[i].ModuleUpdate ();
}
}

public T AddModule<T>() where T : class, IModular, new() {
if (m_GameModuleList.ContainsKey (typeof (T))) {
Util.UnityConsole.LogError ("[GameModuleManager] There was a " + typeof (T).Name);
return null;
} else {
T newModule_ = new T();
newModule_.ModuleInitialize ();
m_GameModuleList.Add (newModule_.GetType (), newModule_);
if (newModule_ is IModuleUpdatable) {
m_GameModuleUpdateList.Add (newModule_ as IModuleUpdatable);
}
return newModule_;
}
}

public void RemoveModule<T>() where T : class, IModular {
if (m_GameModuleList.ContainsKey (typeof (T))) {
m_GameModuleList[typeof (T)].ModuleUninitialize ();
if (m_GameModuleList[typeof (T)] is IModuleUpdatable) {
m_GameModuleUpdateList.Remove (m_GameModuleList[typeof (T)] as IModuleUpdatable);
}
m_GameModuleList.Remove (typeof (T));

} else {
Util.UnityConsole.LogError ("[GameModuleManager] There was no " + typeof (T).Name);
}
}

public T GetModule<T>() where T : class, IModular {
if (m_GameModuleList.ContainsKey (typeof (T))) {
return m_GameModuleList[typeof (T)] as T;
} else {
Util.UnityConsole.LogError ("[GameModuleManager] There was no " + typeof (T).Name);
return null;
}
}
}
}

管理器的實作方式與 GameSystemMono 的管理器大同小異。

在 AddModule() 的時候,必須利用 is 運算子檢查是否有額外的介面被新的 GameModule 所使用。比如發現了 IModuleUpdatable 介面,則要做而外的處理。

對應不同介面,可能也會有特別的 public 方法用於觸發或操作,對於 IModuleUpdatable 介面,我準備了一個 UpdateModule () 來進行觸發。

GameSystemMono 與 GameModule 的比較

GameSystemMono的實作採用繼承 MonoBehaviour,優勢是可以在 Inspector 視窗針對每個組件系統進行參數的設定,並且可以直接使用 Update 等 Unity Message Event 進行動作;缺點則是一定要有個 GameObject 進行載體,而且採用類別繼承的設計,會導致一些彈性上的限制,不需要 MonoBehaviour 功能的組件也會被迫繼承相關特性。

GameModule的實作採用介面 (interface),優勢是可以跟繼承關係並存,切依照需求選擇需要的而外功能介面進行擴充,彈性較高;缺點則是所有的初始化工作就都必須撰寫腳本來完成,而且沒有可以在 Inspector 調整追蹤參數的手段。

就上述的筆記可以看出來,GameSystemMonoGameModule其實是相互補的兩個 Singleton 註冊架構,可以在一個專案中同時採用,依照組件的需求吉特,來決定要包裝成GameSystemMono或是GameModule來管理。

另外還有單一獨立的 Singleton 實作 (如參考所附),如果組件沒有與其他組件互動的需求,只是單單最為一個被呼叫的子系統,也可以採用做為一個簡單易用的方法

單例模式 (Singleton pattern) 是一個相對有較多爭議的設計模式,為了避免在使用的時候會將 Singleton 的缺點凸顯出來,更多的設計考量與深入了解是難免要面對的。

參考