這幾天想把在實習裡碰到的一些好的技巧寫在這裡,也算是對實習的一個總結。好啦,今天要講的是在Unity裡應用一種非常有名的設計模式——單例模式。 開場白 單例模式的簡單介紹請看前面的鏈接,當然網上還有很多更詳細的介紹,有興趣的童靴可以了解一下。其實設計模式對於一個程序員來說還是非常有用的,這點隨著學習的深入感受越來越深。 好啦,現在說一下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) { if(m_Container == null) { 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; using System.Collections; 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 UnityEngine; using UnityEditor; using System.Collections; using System.Collections.Generic; 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; // Use this for initialization 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); } } // Update is called once per frame void Update () { } }