Unity的坑——不要用New来创建继承于MonoBehaviour脚本的对象

直接说最重要的一句话,在Unity中,继承于MonoBehavior的对象,不能使用new关键字来创建,而必须使用AddComponentInstantiate函数来创建,这种对象也要尽量避免使用构造函数,对应的初始化工作要在对应的Awake和Start函数中进行,原因后面再讲。

不要用New来创建继承于MonoBehaviour的对象
对于继承Mono的对象,如果强行使用new创建,得到的结果为null,可以看下面这个例子:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Item1 : MonoBehaviour
{
    public int a;
}

public class Item2
{
    public int b;
}

public class TestE : MonoBehaviour
{
    string s;
    private void Start()
    {
        Item1 item1 = new Item1();
        Item2 item2 = new Item2();
    }
}

Debug进去可以看到item1是空的,而item2是可以被new出来的,如下图所示:

Unity的坑——不要用New来创建继承于MonoBehaviour脚本的对象_第1张图片

只能用AddComponent或Instantiate来创建对象,如下所示

MyObject obj = null;
void Start()
{
  obj = gameObject.AddComponent();
}

或者这么写:

public MyObject objPrefab;
MyObject  obj;
void Start()
{
    obj = Instantiate(objPrefab) as MyObject;
}

避免使用继承MonoBehaviour对象的构造函数
为什么尽量避免使用,可以看下面这个例子:

// 做一个测试的脚本
public class Test : MonoBehaviour
{
    public Test()
    {
        Debug.Log("Constructor is called");
    }

    void Awake()
    {
        Debug.Log("Awake is called");
    }

    void Start()
    {
        Debug.Log("Start is called");
    }
}

然后挂载到场景一物体下,点击Play,输出如下,可以看到Constructor被调用了两次,这显然不是我们想要的

Unity的坑——不要用New来创建继承于MonoBehaviour脚本的对象_第2张图片
甚至在退出游戏后,又调用了一次,如下图所示:
Unity的坑——不要用New来创建继承于MonoBehaviour脚本的对象_第3张图片

而且构造函数也不是在Unity的MainThread中调用的,如果我这么写代码

public Test()
{
    Debug.Log("Constructor is called" + this.gameObject);
}

会报错:

ArgumentException: get_gameObject can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
ConstructorTest..ctor () (at Assets/ConstructorTest.cs:6)

为什么不能用new关键字来创建继承于MonoBehaviour的对象

使用的new的时候会报错:
You are trying to create a MonoBehaviour using the ‘new’ keyword. This is not allowed. MonoBehaviours can only be added using AddComponent(). Alternatively, your script can inherit from ScriptableObject or no base class at all

很好奇为什么不能用new关键字来创建继承于MonoBehaviour的对象,这里先分析一下这些类的继承结构:

GameObject
在C#语言里,所有的对象都有一个源头,就是Object类,Unity里面写的C#脚本也不例外,但Unity里不止有C#的Object,还有类叫做GameObject,GameObject是继承于Object如下所示:

 //
    // Summary:
    //     Base class for all entities in Unity Scenes.
	...
    public sealed class GameObject : Object
    {
    	...
    }

从注释可以看出,GameObject是所有存在于Unity场景内的物体的基类,对于GameObject的对象或子类的对象A,只要A对应的类不继承于MonoBehaviour,我们都可以用new来创建对象A,然后场景对应的Hierarchy里能看到对应的GameObject,

MonoBehaviour
再来分析MonoBehaviour,以下是该脚本的部分代码:

    // Summary:
    //     MonoBehaviour is the base class from which every Unity script derives.
    ...
    public class MonoBehaviour : Behaviour
    {
    	...

MonoBehaviour是所有Unity脚本的基类,对于Behaviour类:

    // Summary:
    //     Behaviours are Components that can be enabled or disabled.
    ...
    public class Behaviour : Component
    {
    	...

Behaviours属于Component,是一种组件,能够被启用或禁用,对于Conponent类:

    // Summary:
    //     Base class for everything attached to GameObjects.
	...
    public class Component : Object
    {
		...

Component回到了C#的Object的怀抱

综合上述脚本可以看出,GameObject就是Unity的Object,而MonoBehaviour,翻译过来叫做单一行为,更形象的说,叫做单一组件,在我理解,任何这种能挂载的,都是Component类的子对象,这也解释了为什么如果我们创建一个类,如果不继承于MonoBehaviour(或者说Component),这个脚本就无法作为Component组件,如下图所示:
Unity的坑——不要用New来创建继承于MonoBehaviour脚本的对象_第4张图片
打开这些组件对应的类的定义,都可以看到Component类的身影,如下图所示
Unity的坑——不要用New来创建继承于MonoBehaviour脚本的对象_第5张图片
Unity的坑——不要用New来创建继承于MonoBehaviour脚本的对象_第6张图片

理论上,如果想要创建脚本,作为挂载在Game Object上的组件,也可以不继承于MonoBehaviour,而是直接继承于Component,应该也是可以的,但是这样做,就无法禁用或启用该脚本,同时也无法正常执行该脚本的Start、Update等函数,但实际上不要这么做,因为这并不符合Unity设计MonoBehaviour的思路,牢记一点,Unity里作为Component用于挂载的脚本组件都应该继承于MonoBehaviour(The MonoBehaviour base class is the only base class from where you can derive your own components from)

总结原因
Unity的代码看似用的C#,实际上底层是翻译成C++使用的,所以不能完全从.Net的角度来看Unity的设计思路,Unity为什么这么设计语言,我也没有找到定论,这里只能做一个大概的猜测,猜测这么做的原因主要有两点:

  • 在C#里,对于Object,用new关键字来创建对象,会调用该类的构造函数,但Unity引擎对于MonoBehaviour类的对象,需要利用其构造函数做一些引擎内的事情,所以不提倡使用new关键字调用其构造函数,而是用Awake和Start函数来代替构造函数的功能
  • 把ScriptComponents组件统一用AddComponent或Instantiate函数来创建,可能是一种类似工厂模式的设计理念,旨在统一管理对象的创建。

相关的设计思路可以参考这些链接:
关于Unity与.NET的一些讨论:
https://forum.unity.com/threads/unity-is-not-net.544795/
关于Component的一些思考:
https://gamedevelopment.tutsplus.com/articles/unity-now-youre-thinking-with-components–gamedev-12492
其他的相关讨论:
https://answers.unity.com/questions/1667835/why-non-monobehaviour-classes-cannot-be-attached-a.html
https://answers.unity.com/questions/445444/add-component-in-one-line-with-parameters.html

你可能感兴趣的:(Unity)