[Unity]构造函数与单例模式

本文通过一个Unity编程中的BUG引入,讨论了为什么不应在继承MonoBehaviour的类中预设构造函数的调用,引用了Unity官方文档的建议,推荐使用Awake或Start进行初始化。同时,提出了一个改进的单例模式实现方式,以确保在不依赖手动赋值给GameObject的情况下保证单例的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从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>{
    //
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值