Unity3d與設計模式(二)單例模式
為什麼要使用單例模式
在我們的整個遊戲生命週期當中,有很多物件從始至終有且只有一個。這個唯一的例項只需要生成一次,並且直到遊戲結束才需要銷燬。
單例模式一般應用於管理器類,或者是一些需要持久化存在的物件。
Unity3d中單例模式的實現方式
(一)c#當中實現單例模式的方法
因為單例本身的寫法不是重點,所以這裡就略過,直接上程式碼。
以下程式碼來自於MSDN。
public sealed class Singleton
{
private static volatile Singleton instance;
private static object syncRoot = new Object();
private Singleton() {}
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
以上程式碼是比較完整版本的c#單例。在unity當中,如果不需要使用到monobeheviour的話,可以使用這種方式來構建單例。
(二)如果是monobeheviour呢?
monobeheviour和一般的類有幾個重要區別,體現在單例模式上有兩點。
第一,monohehaviour不能使用建構函式進行例項化,只能掛載在GameObject上。
第二,當切換場景時,當前場景中的GameObject都會被銷燬(LoadLevel帶有additional引數時除外),這種情況下,我們的單例物件也會被銷燬。
為了使之不被銷燬,我們需要進行DontDestroyOnLoad的處理。同時,為了保持場景當中只有一個例項,我們要對當前場景中的單例進行判斷,如果存在其他的例項,則應該將其全部刪除。
因此,構建單例的方式會變成這樣。
public sealed class SingletonMonoBehaviour: MonoBehaviour
{
private static volatile SingletonMonoBehaviour instance;
private static object syncRoot = new Object();
public static SingletonMonoBehaviour Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null) {
SingletonMonoBehaviour[] instances = (SingletonMonoBehaviour[])FindObjectsOfType(typeof(SingletonMonoBehaviour));
if (instances != null){
for (var i = 0; i < instances.Length; i++) {
Destroy(instances[i].gameObject);
}
}
GameObject go = new GameObject("__SingletonMonoBehaviour");
instance = go.AddComponent<SingletonMonoBehaviour>();
DontDestroyOnLoad(go);
}
}
}
return instance;
}
}
}
這種方式並非完美。其缺陷至少有:
* 如果有許多的單例類,會需要複製貼上這些程式碼
* 有些時候我們也許會希望使用當前存在的所有例項,而不是刪除全部新建一個例項。(這個未必是缺陷,只是設計的不同)
在本文後面將會附上這種單例模式的程式碼以及測試。
(三)使用模板類實現單例
為了避免重複程式碼,我們可以使用模板類的方式來生成單例。非MonoBehaviour的實現方式這裡就不贅述,只說monoBehaviour的。
程式碼
public sealed class SingletonTemplate<T>: MonoBehaviour where T : MonoBehaviour {
private static volatile T instance;
private static object syncRoot = new Object();
public static T Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null) {
T[] instances = (T[])FindObjectsOfType(typeof(T));
if (instances != null){
for (var i = 0; i < instances.Length; i++) {
Destroy(instances[i].gameObject);
}
}
GameObject go = new GameObject();
go.name = typeof(T).Name;
instance = go.AddComponent<T>();
DontDestroyOnLoad(go);
}
}
}
return instance;
}
}
}
以上程式碼解決了每個單例類都需要重複寫同樣程式碼的問題,基本上算一個比較好的解決方案。
單例當中的一些坑
- 最大的坑是單例的monobehaviour,其生命週期並非我們程式設計師可以控制的。MonoBehaviour本身的Destroy,將會決定單例類的例項在何時銷燬。因此,一定不要在OnDestroy函式中呼叫單例物件,這可能導致該物件在遊戲結束後依然存在(原本的單例類已經銷燬了,你又建立了一個新的,當然就不會再銷燬一次了)。舉例來說,以下的程式碼是需要注意的的。
void Start(){
Singleton.Instance.OnSomeTime += DoSth;
}
void OnDestroy(){
Singleton.Instance.OnSomeTime -= DoSth;
}
- 此外,建議不要在場景或者預置當中放置擁有單例類元件的Gameobject。很多網上的專案有這樣的寫法。但我的觀點是這種寫法不夠靈活。如果使用這種方法,注意在獲取instance時,將找到的第一個物件賦給instance
if (instance == null) {
T[] instances = (T[])FindObjectsOfType(typeof(T));
if (instances != null){
instance = instances[0];
for (var i = 1; i < instances.Length; i++) {
Destroy(instances[i].gameObject);
}
}
}
單例與靜態的區別
我們都知道,靜態的成員或者方法,在整個Runtime當中也只有一份。所以一直存在著靜態與單例模式之爭。
事實上這兩種方式都有其適用範圍,不能片面的說某種好或某種不好。具體的爭論實在是太多了,資料也多,這裡也不深入講,僅僅簡單的說明一下兩者使用上的區別。
* 單例的方法可以繼承,靜態的不可以。
* 單例存在著建立例項的過程,生命週期並不是整個執行時,靜態方法在編譯時就存在,整個過程中是一直有效的。
雖然兩者的區別其實非常多,但在這裡只說一個最核心的問題,如何進行選擇?
其實很簡單,從面向物件的角度來說——
* 如果方法中需要用到例項本身的狀態,也就是說需要用到例項的成員時,這個方法一定是例項方法,請使用單例呼叫。
* 如果方法中完全不涉及到例項,而是類共享的一些狀態的話,或者甚至不需要任何狀態,這個方法一定是靜態方法。
從應用的角度來說,我覺得以上就足夠了,至於說記憶體佔用的不同啊,GC以及效率上的區別啊這些我覺得更多是理論,不夠貼近實際使用。
單例雖好,請勿濫用
濫用設計模式是很多人都會遇到的問題,尤其是對新手來說。設計模式應該只在合適的場景當中使用,而不是隨處都使用單例。
事實上,單例的濫用會造成以下一些問題:
* 程式碼的耦合性可能會增加。如一個模組當中呼叫MusicController.instance.Play,可能導致這個模組無法獨立複用。
* 單個類的職責可能會過大,違背單一職責原則。
* 某些情況下會造成一些效能問題。
可以使用一些別的方法來代替單例模式,這裡暫時不再擴充套件。
不使用單例的單例
在某些情況下我會使用這種方法來構建唯一例項。
Game.Instance.MusicController或Game.MusicController。
作為更高一級的控制器的單例成員或者類變數,同樣可以使該例項在整個遊戲中僅存在一份。
其優勢在於擴充套件性更好,因為我們可以隨時新增Game.Instance.ReleaseMusicController,等等。這裡就不再擴充套件了。
本文的程式碼如下
singleton