單例模式的簡單介紹請看前面的連結,當然網上還有很多更詳細的介紹,有興趣的童靴可以了解一下。其實設計模式對于一個程式員來說還是非常有用的,這點随着學習的深入感受越來越深。
好啦,現在說一下Unity裡的單例模式。什麼時候需要使用單例模式呢?正如它的名字一樣,你認為一些東西在整個遊戲中隻有一個而你又想可以友善地随時通路它,這時你就可以考慮單例模式了。例如,你的遊戲可能需要一個管理音樂播放的腳本,或者一個管理場景切換的腳本,或者一個管理玩家資訊的通用腳本,又或者是管理遊戲中各種常用UI的腳本。事實上,這些都是非常常用而且必要的。
慶幸的是,單例模式的代碼非常簡單。下面是Singleton.cs的内容:
using System;
using System.Collections;
using System.Collections.Generic;
public class Singleton : MonoBehaviour
{
private static GameObject m_Container = null;
private static string m_Name = "Singleton";
private static Dictionary<string, object> m_SingletonMap = new Dictionary<string, object>();
private static bool m_IsDestroying = false;
public static bool IsDestroying
{
get { return m_IsDestroying; }
}
public static bool IsCreatedInstance(string Name)
if(m_Container == null)
{
return false;
}
if (m_SingletonMap!=null && m_SingletonMap.ContainsKey(Name))
return true;
return false;
public static object getInstance (string Name)
Debug.Log("Create Singleton.");
m_Container = new GameObject ();
m_Container.name = m_Name;
m_Container.AddComponent (typeof(Singleton));
if (!m_SingletonMap.ContainsKey(Name)) {
if(System.Type.GetType(Name) != null)
{
m_SingletonMap.Add(Name, m_Container.AddComponent (System.Type.GetType(Name)));
}
else
Debug.LogWarning("Singleton Type ERROR! (" + Name + ")");
return m_SingletonMap[Name];
}
public void RemoveInstance(string Name)
if (m_Container != null && m_SingletonMap.ContainsKey(Name))
UnityEngine.Object.Destroy((UnityEngine.Object)(m_SingletonMap[Name]));
m_SingletonMap.Remove(Name);
Debug.LogWarning("Singleton REMOVE! (" + Name + ")");
void Awake ()
Debug.Log("Awake Singleton.");
DontDestroyOnLoad (gameObject);
void Start()
Debug.Log("Start Singleton.");
void Update()
void OnApplicationQuit()
Debug.Log("Destroy Singleton");
if(m_Container != null)
GameObject.Destroy(m_Container);
m_Container = null;
m_IsDestroying = true;
}
}
代碼大部分都比較容易看懂,下面介紹幾點注意的地方:
當我們在其他代碼裡需要通路某個單例時,隻需調用getInstance函數即可,參數是需要通路的腳本的名字。我們來看一下這個函數。它首先判斷所有單例所在的容器m_Container是否為空(實際上就是場景中是否存在一個Gameobject,上面捆綁了一個Singleton腳本),如果為空,它将自動建立一個對象,然後以“Singleton”命名,再捆綁Singleton腳本。m_SingletonMap是負責維護所有單例的映射。當第一次通路某個單例時,它會自動向m_Container上添加一個該單例類型的Component,并儲存在單例映射中,再傳回這個單例。是以,我們可以看出,單例的建立完全都是自動的,你完全不需要考慮在哪裡、在什麼時候捆綁腳本,這是多麼令人高興得事情!
在Awake函數中,有一句代碼DontDestroyOnLoad (gameObject);,這是非常重要的,這句話意味着,當我們的場景發生變化時,單例模式将不受任何影響。除此之外,我們還要注意到,這句話也必須放到Awake函數,而不能放到Start函數中,這是由兩個函數的執行順序決定的,如果反過來,便可能會造成通路單例不成功,下面的例子裡會更詳細的介紹;
在OnApplicationQuit函數中,我們将銷毀單例模式。
最後一點很重要:一定不要在OnDestroy函數中直接通路單例模式!這樣很有可能會造成單例無法銷毀。這是因為,當程式退出準備銷毀單例模式時,我們在其他腳本的OnDestroy函數中再次請求通路它,這樣将重新構造一個新的單例而不會被銷毀(因為之前已經銷毀過一次了)。如果一定要通路的話,一定要先調用IsCreatedInstance,判斷該單例是否存在。
下面,我們通過一個小例子來示範單例模式的使用。
首先,我們需要建立如上的Singleton腳本。然後,再建立一個新的腳本SingletonSample.cs用于測試,其内容如下:
using UnityEngine;
public class SingletonSample : MonoBehaviour {
// Use this for initialization
void Start () {
TestSingleton();
// Update is called once per frame
void Update () {
private void TestSingleton() {
LitJsonSample litjson = Singleton.getInstance("LitJsonSample") as LitJsonSample;
litjson.DisplayFamilyList();
// void OnDestroy() {
// LitJsonSample litjson = Singleton.getInstance("LitJsonSample") as LitJsonSample;
//
// litjson.DisplayFamilyList();
// }
注意,為了友善,我使用了上一篇博文裡使用的Litjson的代碼,并做了少許修改。下面是修改後的LitJsonSample.cs:
using UnityEditor;
using LitJson;
public class FamilyInfo {
public string name;
public int age;
public string tellphone;
public string address;
public class FamilyList {
public List<FamilyInfo> family_list;
public class LitJsonSample : MonoBehaviour {
public FamilyList m_FamilyList = null;
void Awake () {
ReloadFamilyData();
private void ReloadFamilyData()
//AssetDatabase.ImportAsset("Localize/family.txt");
UnityEngine.TextAsset s = Resources.Load("Localize/family") as TextAsset;
string tmp = s.text;
m_FamilyList = JsonMapper.ToObject<FamilyList>( tmp );
if ( JsonMapper.HasInterpretError() )
Debug.LogWarning( JsonMapper.GetInterpretError() );
public void DisplayFamilyList() {
if (m_FamilyList == null) return;
foreach (FamilyInfo info in m_FamilyList.family_list) {
Debug.Log("Name:" + info.name + " Age:" + info.age + " Tel:" + info.tellphone + " Addr:" + info.address);
然後,将SingletonSample.cs添加到場景中的一個對象上。我偷懶就直接添加到了錄影機上。注意,其他兩個代碼不要添加到任何對象上。
運作結果如圖:

為了證明之前所說的不要在OnDestroy函數裡通路單例模式,我們把SingletonSample.cs腳本裡注釋掉得OnDestroy函數解開注釋,然後再次運作。結果如下:
我們注意到,除了Log頁面裡出現了錯誤資訊外,右側的場景面闆裡也多了一個Singleton對象(這是我已經停止運作了)。從Log資訊裡,我們可以發現,在第一次銷毀掉單例模式後,單例模式又再次被建立,但卻沒有被銷毀,由此便殘留在了面闆裡。
正确的做法是,在OnDestroy函數裡加一層安全性判斷,如下:
void OnDestroy() {
if (Singleton.IsCreatedInstance("LitJsonSample")) {
這樣,就可以得到正确結果了。
最後,還有幾句話要啰嗦一下,雖然和單例模式的關系不大,嘿嘿。我們需要注意一下Start函數和Awake函數的執行順序。在這個例子裡,我在LitJsonSample.cs的Awake函數裡調用了ReloadFamilyData來初始化資料,細心的童鞋可以發現,在上一篇博文裡,初始化資料是在Start函數裡完成的。之是以要把它挪到Awake函數裡,是為了在我們通路單例時,可以保證資料一定已經被初始化了,是以把初始化函數放到Awake函數裡,通路單例的代碼放在Start函數裡。同樣的原因,在Singleton.cs的腳本裡DontDestroyOnLoad (gameObject);需要放在Awake函數,而不是Start函數裡。
關于Awake函數和Start函數的執行順序,可以詳見腳本說明。簡單來說,Awake函數在這個腳本在場景中加載時就會調用,至于所有腳本的Awake函數的調用順序是未知的。然後,在所有的Awake函數調用完畢後,才開始調用Start函數。需要注意的是,Start函數也不是一定立即執行的,它是在該腳本第一次調用Update函數之前調用的,也就是說,如果這個腳本一開始的狀态是disable的,那麼直到它變成enable狀态,在Update函數第一次執行前,才會執行Start函數。兩個函數的執行順序是時間有時正是某些Bug的産生原因!而且這些Bug往往很難發現。
哈,我這次實習的面試時,面試的姐姐就問過我這個問題,希望大家也可以搞清楚,如果我這裡有說的不對的,請指正。
好啦,這次就到這裡,謝謝閱讀!
本文轉蓬萊仙羽51CTO部落格,原文連結:http://blog.51cto.com/dingxiaowei/1366122,如需轉載請自行聯系原作者