小言_互联网的博客

Unity踩坑记录:如果继承MonoBehaviour,类的构造函数可能会被Unity调用多次,不要在构造函数做初始化工作

1118人阅读  评论(0)

先上Unity官方文档

有经验的程序员可能会惊讶于没有使用构造函数来完成对象的初始化。这是因为对象的构造由编辑器处理,不会像您可能期望的那样在游戏运行过程开始时进行。如果尝试为脚本组件定义构造函数,将会干扰 Unity 的正常运行,并可能导致项目出现重大问题。

似乎以前的官方文档(?不知道是不是)还有更加明确的说明:

Avoid using the constructor. 避免使用构造函数

Never initialize any values in the constructor. Instead use Awake or Start for this purpose. Unity automatically invokes the constructor even when in edit mode. This usually happens directly after compilation of a script, because the constructor needs to be invoked in order to retrieve default values of a script. Not only will the constructor be called at unforeseen times, it might also be called for prefabs or inactive game objects.
不要在构造函数中初始化任何变量.要用Awake或Start函数来实现.即便是在编辑模式,Unity仍会自动调用构造函数.这通常是在一个脚本编译之后,因为需要调用脚本的构造函数来取回脚本的默认值.我们无法预计何时调用构造函数,它或许会被预置体或未激活的游戏对象所调用.

In the case of eg. a singleton pattern using the constructor this can have severe consequences and lead to seemingly random null reference exceptions.
单一模式使用构造函数可能会导致严重后果,会带来类似随机的空参数异常.

So if you want to implement eg. a singleton pattern do not use the the constructor, instead use Awake . Actually there is no reason why you should ever have any code in a constructor for a class that inherits from MonoBehaviour .
因此,如果你想实现单一模式不要用构造函数,要用Awake函数.事实上,你没必要在继承自MonoBehaviour的类的构造函数中写任何代码.

所以这是一个什么问题?

为了更好地说明,这里直接举个例子:

public class TestScript : MonoBehaviour 
{
    public TestScript()
    { 
        print("调用构造函数");
    }
}

当该脚本被挂载于一个物体时启动游戏,会发现控制台输出多次“调用构造函数”(我测试时是3次)

简而言之就是构造的语句会被执行多次

所以它会造成什么问题?

当尝试在继承了Monobehaviour的对象的构造过程中做一些实质性写入操作时候,就可能会引发不可预估的错误
举个例子:

public class TestScript : MonoBehaviour 
{
	static int integer;
	
    public TestScript()
    { 
        integer++;
    }
}

可见如果我们需要通过静态变量integer来进行计数的话,由于构造函数被调用了多次,这个integer也会被++多次,最后的结果和我们想要的结果很明显是大相径庭的,而且这种逻辑错误很难被发现(没踩过坑的谁会知道unity还有这种设定)

如何解决这个问题?

如官方文档所述,只需要把所有的初始化语句写到unity自带的Awake或者Start函数就可以了:

public class TestScript : MonoBehaviour 
{
	static int integer;
	
    public TestScript()
    { 
        integer++;//被++多次
    }
    void Awake()
    {
        integer++;//只++一次
    }
}

虽然构造函数TestScript()仍然被调用了多次,但Awake()只会被调用一次,这是我们可以控制的。所以如果我们删掉构造函数,只保留Awake(),其实大部分情况下可以实现和构造函数本应有的效果。(除非你想在构造时从构造函数传入参数,但事实上继承Monobehaviour的类基本不会用到这种形式)

值得注意的地方

继承了Monobehaviour的类,并不止构造函数,而是整个构造过程都会被unity灵性执行多次
所以下面的例子事实上存在两个问题:

public class TestScript : MonoBehaviour 
{
	TestObject1 testObject1 = new TestObject1();//被new多次,事实上产生了多个对象
	TestObject2 testObject2;
    public TestScript()
    { 
        testObject2 = new TestObject2();//被new多次,事实上产生了多个对象
    }
}

除了在构造函数TestScript()内的语句被执行了多次,TestScript 类成员的声明语句也会在构造时被Unity执行多次,导致在构造函数外的成员testObject1事实上也被new了多次,这会导致额外产生了若干个多余TestObject1对象。一个类成员(如testObject1)被初始化多次也可能会引发不可预估的错误(这里则取决于TestObject1本身构造过程的某些语句),所以注意,也不要在类声明中直接初始化成员

上述问题的“解决”方法:

仍然可以用Awake()初始化对象

public class TestScript : MonoBehaviour 
{
	TestObject1 testObject1;//只作声明
	TestObject2 testObject2;//只作声明
    void Awake()
    {
        testObject1 = new TestObject1();//在Awake中初始化,只会new一次
        testObject2 = new TestObject2();//同样只new一次
    }
}

总结

问题描述:

在Unity中,继承于Monobehaviour的类在实例化时,所有构造语句都可能会被Unity执行多次(包括成员声明和构造函数),我们无法避免这一过程。所以在这些类的构造过程中不要进行任何初始化,否则可能会导致不可预估的错误。
注意:这并不是你的编程习惯出了问题,而是Unity机制的问题。 如果不继承Monobehaviour请大胆在成员声明或构造函数中进行初始化,所以……

解决办法:

用Awake()或Start()代替构造函数!
用Awake()或Start()代替构造函数!
用Awake()或Start()代替构造函数!
或者
不要继承Monobehaviour!
不要继承Monobehaviour!
不要继承Monobehaviour!


转载:https://blog.csdn.net/mkr67n/article/details/108021754
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场