[Unity3d]单例模式

这几天想把在实习里碰到的一些好的技巧写在这里,也算是对实习的一个总结。好啦,今天要讲的是在Unity里应用一种非常有名的设计模式——单例模式

开场白

单例模式的简单介绍请看前面的链接,当然网上还有很多更详细的介绍,有兴趣的童靴可以了解一下。其实设计模式对于一个程序员来说还是非常有用的,这点随着学习的深入感受越来越深。

好啦,现在说一下Unity里的单例模式。什么时候需要使用单例模式呢?正如它的名字一样,你认为一些东西在整个游戏中只有一个而你又想可以方便地随时访问它,这时你就可以考虑单例模式了。例如,你的游戏可能需要一个管理音乐播放的脚本,或者一个管理场景切换的脚本,或者一个管理玩家信息的通用脚本,又或者是管理游戏中各种常用UI的脚本。事实上,这些都是非常常用而且必要的。

实现

庆幸的是,单例模式的代码非常简单。下面是Singleton.cs的内容:

[csharp] view
plain
copyprint?

  1. using System;  
  2. using System.Collections;  
  3. using System.Collections.Generic;  
  4.    
  5.    
  6. public class Singleton : MonoBehaviour  
  7. {  
  8.     private static GameObject m_Container = null;  
  9.     private static string m_Name = "Singleton";  
  10.     private static Dictionary<string, object> m_SingletonMap = new Dictionary<string, object>();  
  11.     private static bool m_IsDestroying = false;  
  12.        
  13.     public static bool IsDestroying  
  14.     {  
  15.         get { return m_IsDestroying; }  
  16.     }  
  17.        
  18.     public static bool IsCreatedInstance(string Name)  
  19.     {  
  20.         if(m_Container == null)  
  21.         {  
  22.             return false;  
  23.         }  
  24.         if (m_SingletonMap!=null && m_SingletonMap.ContainsKey(Name))   
  25.         {  
  26.             return true;  
  27.         }  
  28.         return false;  
  29.            
  30.     }  
  31.     public static object getInstance (string Name)  
  32.     {  
  33.         if(m_Container == null)  
  34.         {  
  35.             Debug.Log("Create Singleton.");  
  36.             m_Container = new GameObject ();  
  37.             m_Container.name = m_Name;      
  38.             m_Container.AddComponent (typeof(Singleton));  
  39.         }  
  40.         if (!m_SingletonMap.ContainsKey(Name)) {  
  41.             if(System.Type.GetType(Name) != null)  
  42.             {  
  43.                 m_SingletonMap.Add(Name, m_Container.AddComponent (System.Type.GetType(Name)));  
  44.             }  
  45.             else  
  46.             {  
  47.                 Debug.LogWarning("Singleton Type ERROR! (" + Name + ")");  
  48.             }  
  49.         }  
  50.         return m_SingletonMap[Name];  
  51.     }     
  52.        
  53.     public void RemoveInstance(string Name)  
  54.     {  
  55.         if (m_Container != null && m_SingletonMap.ContainsKey(Name))  
  56.         {  
  57.             UnityEngine.Object.Destroy((UnityEngine.Object)(m_SingletonMap[Name]));  
  58.             m_SingletonMap.Remove(Name);  
  59.               
  60.             Debug.LogWarning("Singleton REMOVE! (" + Name + ")");  
  61.         }  
  62.     }  
  63.    
  64.     void Awake ()  
  65.     {  
  66.         Debug.Log("Awake Singleton.");  
  67.         DontDestroyOnLoad (gameObject);  
  68.     }  
  69.        
  70.     void Start()  
  71.     {  
  72.         Debug.Log("Start Singleton.");  
  73.     }     
  74.        
  75.     void Update()  
  76.     {  
  77.     }  
  78.        
  79.     void OnApplicationQuit()  
  80.     {  
  81.         Debug.Log("Destroy Singleton");  
  82.         if(m_Container != null)  
  83.         {  
  84.             GameObject.Destroy(m_Container);  
  85.             m_Container = null;  
  86.             m_IsDestroying = true;  
  87.         }             
  88.     }  
  89.        
  90. }  

代码大部分都比较容易看懂,下面介绍几点注意的地方:

  • 当我们在其他代码里需要访问某个单例时,只需调用getInstance函数即可,参数是需要访问的脚本的名字。我们来看一下这个函数。它首先判断所有单例所在的容器m_Container是否为空(实际上就是场景中是否存在一个Gameobject,上面捆绑了一个Singleton脚本),如果为空,它将自动创建一个对象,然后以“Singleton”命名,再捆绑Singleton脚本。m_SingletonMap是负责维护所有单例的映射。当第一次访问某个单例时,它会自动向m_Container上添加一个该单例类型的Component,并保存在单例映射中,再返回这个单例。因此,我们可以看出,单例的创建完全都是自动的,你完全不需要考虑在哪里、在什么时候捆绑脚本,这是多么令人高兴得事情!
  • 在Awake函数中,有一句代码DontDestroyOnLoad (gameObject);,这是非常重要的,这句话意味着,当我们的场景发生变化时,单例模式将不受任何影响。除此之外,我们还要注意到,这句话也必须放到Awake函数,而不能放到Start函数中,这是由两个函数的执行顺序决定的,如果反过来,便可能会造成访问单例不成功,下面的例子里会更详细的介绍;
  • 在OnApplicationQuit函数中,我们将销毁单例模式。
  • 最后一点很重要:一定不要在OnDestroy函数中直接访问单例模式!这样很有可能会造成单例无法销毁。这是因为,当程序退出准备销毁单例模式时,我们在其他脚本的OnDestroy函数中再次请求访问它,这样将重新构造一个新的单例而不会被销毁(因为之前已经销毁过一次了)。如果一定要访问的话,一定要先调用IsCreatedInstance,判断该单例是否存在。

例子

下面,我们通过一个小例子来演示单例模式的使用。

首先,我们需要创建如上的Singleton脚本。然后,再创建一个新的脚本SingletonSample.cs用于测试,其内容如下:

[csharp] view
plain
copyprint?

  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class SingletonSample : MonoBehaviour {  
  5.   
  6.     // Use this for initialization  
  7.     void Start () {  
  8.         TestSingleton();  
  9.     }  
  10.       
  11.     // Update is called once per frame  
  12.     void Update () {  
  13.       
  14.     }  
  15.       
  16.     private void TestSingleton() {  
  17.         LitJsonSample litjson = Singleton.getInstance("LitJsonSample") as LitJsonSample;  
  18.           
  19.         litjson.DisplayFamilyList();  
  20.     }  
  21.       
  22. //  void OnDestroy() {  
  23. //      LitJsonSample litjson = Singleton.getInstance("LitJsonSample") as LitJsonSample;  
  24. //        
  25. //      litjson.DisplayFamilyList();  
  26. //  }  
  27. }  

注意,为了方便,我使用了上一篇博文里使用的Litjson的代码,并做了少许修改。下面是修改后的LitJsonSample.cs:

[csharp] view
plain
copyprint?

  1. using UnityEngine;  
  2. using UnityEditor;  
  3. using System.Collections;  
  4. using System.Collections.Generic;  
  5. using LitJson;  
  6.   
  7. public class FamilyInfo {  
  8.     public string name;  
  9.     public int age;  
  10.     public string tellphone;  
  11.     public string address;  
  12. }  
  13.   
  14. public class FamilyList {  
  15.     public List<FamilyInfo> family_list;  
  16. }  
  17.   
  18. public class LitJsonSample : MonoBehaviour {  
  19.       
  20.     public FamilyList m_FamilyList = null;  
  21.       
  22.     // Use this for initialization  
  23.     void Awake () {  
  24.         ReloadFamilyData();  
  25.     }  
  26.       
  27.     private void ReloadFamilyData()  
  28.     {  
  29.         //AssetDatabase.ImportAsset("Localize/family.txt");  
  30.               
  31.         UnityEngine.TextAsset s = Resources.Load("Localize/family") as TextAsset;   
  32.         string tmp = s.text;  
  33.         m_FamilyList = JsonMapper.ToObject<FamilyList>( tmp );  
  34.         if ( JsonMapper.HasInterpretError() )  
  35.         {  
  36.             Debug.LogWarning( JsonMapper.GetInterpretError() );  
  37.         }  
  38.     }  
  39.       
  40.     public void DisplayFamilyList() {  
  41.         if (m_FamilyList == null) return;  
  42.           
  43.         foreach (FamilyInfo info in m_FamilyList.family_list) {  
  44.             Debug.Log("Name:" + info.name + "       Age:" + info.age + "        Tel:" + info.tellphone + "      Addr:" + info.address);  
  45.         }  
  46.     }  
  47.       
  48.     // Update is called once per frame  
  49.     void Update () {  
  50.       
  51.     }  
  52. }  

然后,将SingletonSample.cs添加到场景中的一个对象上。我偷懒就直接添加到了摄像机上。注意,其他两个代码不要添加到任何对象上。

运行结果如图:

为了证明之前所说的不要在OnDestroy函数里访问单例模式,我们把SingletonSample.cs脚本里注释掉得OnDestroy函数解开注释,然后再次运行。结果如下:

我们注意到,除了Log页面里出现了错误信息外,右侧的场景面板里也多了一个Singleton对象(这是我已经停止运行了)。从Log信息里,我们可以发现,在第一次销毁掉单例模式后,单例模式又再次被创建,但却没有被销毁,由此便残留在了面板里。

正确的做法是,在OnDestroy函数里加一层安全性判断,如下:

[csharp] view
plain
copyprint?

  1. void OnDestroy() {  
  2.     if (Singleton.IsCreatedInstance("LitJsonSample")) {  
  3.         LitJsonSample litjson = Singleton.getInstance("LitJsonSample") as LitJsonSample;  
  4.               
  5.         litjson.DisplayFamilyList();  
  6.     }  
  7. }  

这样,就可以得到正确结果了。

结束语

最后,还有几句话要啰嗦一下,虽然和单例模式的关系不大,嘿嘿。我们需要注意一下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往往很难发现。

哈,我这次实习的面试时,面试的姐姐就问过我这个问题,希望大家也可以搞清楚,如果我这里有说的不对的,请指正。

好啦,这次就到这里,谢谢阅读!

转载自:http://blog.csdn.net/candycat1992/article/details/10960731

关于单利模式,可以参考Momo的一篇文章中有提到:http://www.xuanyusong.com/archives/1948#0-qzone-1-78151-d020d2d2a4e8d1a374a433f596ad1440d020d2d2a4e8d1a374a433f596ad1440

时间: 2024-10-27 23:51:13

[Unity3d]单例模式的相关文章

scala实现单例模式

单例模式介绍 单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中一个类只有一个实例. 对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务:一个系统只能有一个窗口管理器或文件系统:一个系统只能有一个计时工具或ID(序号)生成器.如在Windows中就只能打开一个任务管理器.如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源:

io-dom4j中因为输出流未正确关闭,导致再次获取单例模式的document时,对象为null

问题描述 dom4j中因为输出流未正确关闭,导致再次获取单例模式的document时,对象为null 一:如下是获取单例模式的document对象. private static Document returndoc(Document docuname, String xmlname) { if (docuname != null) { return docuname; } SAXReader reader = new SAXReader(); // 如果不存在就创建对象. try { docu

[设计模式实践之路](1)单例模式

[简介] 单例模式(Singleton)保证一个类仅有一个实例,并提供一个访问它的全局访问点.      实现单例模式的一个最好的方法就是让类自身负责保存它的唯一实例.这个类可以保证没有其他实例可以创建,并且它可以提供一个访问该实例的方法. [特点] 单例模式具有一下特点: 单例类只有一个实例 单例类必须自己创建自己的唯一实例 单例类必须给所有其他对象提供这一实例 [分类] 主要的就是懒汉单例,饿汉单例 [懒汉单例] package Mode; /** * Java设计模式之单例模式 * @au

C# 单例模式的五种写法

C# 单例模式的五种写法及优劣分析,见下文: [单例模式及常见写法](http://blog.csdn.net/jiankunking/article/details/50867050)

ios-Unide3d 导出的Xcode工程能不能查看unity3d里的脚本

问题描述 Unide3d 导出的Xcode工程能不能查看unity3d里的脚本 Unide3d导出了Xcode工程,能不能就在Xcode工程里查看哪些js文件,如果看不了那这些脚本是怎么在Xcode下运行的呢?求大神解答

unity3d-关于Unity3d WWW 加载本地资源的问题

问题描述 关于Unity3d WWW 加载本地资源的问题 WWW www = new WWW (""file://E:/项目/Assets/StreamingAssets/Actor.assetbundle"");WWW www = new WWW (""file:///E:/项目/Assets/StreamingAssets/Actor.assetbundle"");两种都试过都不行总是提示 You are trying t

如何正确地写出单例模式

单例模式算是设计模式中最容易理解,也是最容易手写代码的模式了吧.但是其中的坑却不少,所以也常作为面试题来考.本文主要对几种单例写法的整理,并分析其优缺点.很多都是一些老生常谈的问题,但如果你不知道如何创建一个线程安全的单例,不知道什么是双检锁,那这篇文章可能会帮助到你. 懒汉式,线程不安全 当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的. public class Singleton { private static Singleton instan

Unity3D中暂停时的动画及粒子效果实现

暂停是游戏中经常出现的功能,而Unity3D中对于暂停的处理并不是很理想.一般的做法是将Time.timeScale设置为0.Unity的文档中对于这种情况有以下描述: The scale at which the time is passing. This can be used for slow motion effects-.When timeScale is set to zero the game is basically paused - timeScale表示游戏中时间流逝快慢的尺

在Win7上怎么样编译unity3d mono?

问题描述 在Win7上怎么样编译unity3d mono? 网络上有一些用Cgywin编译Unity3D mono的文章,但照着去做总是有一些各种各样的问题,很难成功编译,不知道谁有完整的方法. 解决方案 http://www.xuebuyuan.com/2060835.html