从BUG说起
在实现一个小功能时,遇到了一个bug,代码如下:
public class EnemySpawner : MonoBehaviour {
#region singleton
private EnemySpawner() {}
private static EnemySpawner _instance = null;
public static EnemySpawner Instance {
get {
if(_instance == null)
_instance = new EnemySpawner();
return _instance;
}
}
#endregion
//remianing
void Update(){
//spwan enemies
}
}
实现了一个敌人孵化器的类,由于它应该是存在于全局,且仅有一份,所以将其写作单例模式。
上述代码是不考虑多线程问题的单例实现。
将其挂载到一个空物体上,运行,但是出现了一系列的问题。
在调试过程中发现,在Instance的get中,即便new EnemySpawner(),__instance仍然为null??!!
我百思不得解,而且神奇的是,当我写下如下代码测试的时候:
public class AudioManager : MonoBehaviour {
public AudioManager(){
Debug.Log("You call me in AudioManager");
}
}
当我开始运行游戏时,Log输出了两次”You call me in AudioManager”.
只好借助引擎了..
永远不要在继承MonoBehaviour的类中预设构造函数的调用
其实,在Unity的文档中有提到上述问题:
“避免使用构造函数 不要在构造函数中初始化任何变量,使用Awake或Start实现这个目的。即使是在编辑模式中Unity也自动调用构造函数,这通常发生在一个脚本被编译之后,因为需要调用构造函数来取向一个脚本的默认值。构造函数不仅会在无法预料的时刻被调用,它也会为预设或未激活的游戏物体调用。”
实际上,MonoBehaviour有两个生命周期,一个是作为C#对象的周期,一个是作为Component的周期。构造函数代表第一个,Awake代表第二个。Editor环境下Editor的代码和脚本代码在同一个AppDomain里,对象的生命周期会表现的跟Player环境下不一样。比如Editor中构造函数被调用的次数和时机跟build出来的游戏不一样,这样就不容易保证正确性。
而且,在编辑器模式下,脚本类就会被构造,在你进入游戏之后,也会被构造,你无法预期类的构造函数何时被调用,因此,凡是继承自MonoBehaviour的类,把它的Awake或者Update当做构造函数来初始化变量。
那么,如何改进上述代码呢?
public class EnemySpawner : MonoBehaviour {
#region singleton
private EnemySpawner() {}
private static EnemySpawner _instance = null;
public static EnemySpawner Instance {
get {
return _instance;
}
}
#endregion
private void Awake() {
_instance = this;
}
}
不考虑多线程等问题,这样改进应当是足够的。
有人可能会指出,不是不可以使用构造函数吗,你怎么还private EnemySpawner() {}
这样写呢?实际上,这只是对它的可访问性进行了限制,并没有使用new EnemySpawner()
。
但是,这种单例实现有个缺陷,就是你必须这种将脚本手动赋给一个GameObject(缺陷)。
更好的单例实现方式
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour{
private static T instance;
public static T Instance {
get {
if(instance == null){
GameObject obj = new GameObject();
instance = obj.AddComponent<T>();
obj.name = typeof(T).Name;
//切换场景之后不销毁单例对象
DontDestroyOnLoad(obj);
}
return instance;
}
}
public class AudioManager : Singleton<AudioMananger>{
//
}