• Unity 丨 避免用New来创建继承于MonoBehaviour脚本的对象


    直接说最重要的一句话,在Unity中,继承于MonoBehavior的对象,要避免使用new关键字来创建,而必须使用AddComponent或Instantiate函数来创建,这种对象也要尽量避免使用构造函数,对应的初始化工作要在对应的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是null,而item2是可以被new出来的,如下图所示:

     

    (顺便说一句,上图这里item1虽然显示的"null",但并不意味着item1对象是null,有兴趣的可以看后面的附录)

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

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

    或者这么写:

    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的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的时候会报错(也有的Unity版本会给一个警告,但不报错):

    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,我们都可以用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回到了UnityEngine.Object的怀抱


    综合上述脚本可以看出,GameObject就是Unity在Scene里的实体,而MonoBehaviour,翻译过来叫做单一行为,更形象的说,叫做单一组件,在我理解,任何这种能挂载的,都是Component类的子对象,这也解释了为什么如果我们创建一个类,如果不继承于MonoBehaviour(或者说Component),这个脚本就无法作为Component组件,如下图所示:

    打开这些组件对应的类的定义,都可以看到Component类的身影,如下图所示

    理论上,如果想要创建脚本,作为挂载在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函数来代替构造函数的功能。举个例子,下面部分是MonoBehaviour在C#这边的部分源码,可以看到,引擎自己在里面使用了构造函数:
    // MonoBehaviour.bindings.cs文件夹

    namespace UnityEngine
    {
    // MonoBehaviour is the base class every script derives from.
    [RequiredByNativeCode]
    [ExtensionOfNativeClass]
    [NativeHeader("Runtime/Mono/MonoBehaviour.h")]
    [NativeHeader("Runtime/Scripting/DelayedCallUtility.h")]
    public class MonoBehaviour : Behaviour
    {
    public MonoBehaviour()
    {
    ConstructorCheck(this);
    }
    ...
    }
    }

    把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


    附录
    感谢评论区提醒:继承于MonoBehaviour的类,使用new得到的对象其实不是null,其method(在C#这边的)是可以调用的。

    我跑了一些脚本,发现确实如此,搜了一下原因,这是C#的System.Object与Unity的UnityEngine.Object的不同导致的,可以看下面的截图,这里的item1其实并不是null,而是它的ToString()返回了一个"null"字符串,所以这里显示为"null",这营造了一种item1是null的假象。不过,虽然在C#这边看,它不是null,而在C++这边看,它是null。


    至于为什么是这样,原因比较复杂,这里简单说一下。

    UnityEngine.Object是Unity里的特殊类,它不同于C#的System.Object,UnityEngine.Object会被Link到对应C++的对象上。比如说对于Camera这个Component,它继承于UnityEngine.Object类,其内部数据实际上在C++端,不在C#这边的Object上,C#这边可以当作一个handle处理的,可以通过C#上的handle找到C++端对应的数据。

    当我使用new创建一个MonoBehaviour对象时,该对象在C#这边被创建好了,但是在C++这边的数据是没有的,像MonoBehaviour对象的enable这种field,是存在于C++端的,此时如果取C++端的数据,就会抛出异常。

    举个简单例子:

    // 我创建的SomeMonoBehaviour, 其实有两种函数和数据, 一种函数和数据放在C++端, 比如enabled数据
    public class SomeMonoBehaviour : MonoBehaviour
    {
    // 第二种函数和数据放在C#端, 这些东西都是可以调用的(不过函数不一定调用会正确, 因为它的数据可能在C++端不存在)
    internal bool someField;
    }
    
    public class Test : MonoBehaviour
    {
    
    [SerializeField]
    // SomeMonoBehaviour在C#端的数据是由GC销毁的, 由于这里一直记录着引用, 所以someField的数据永远可以获取
    private SomeMonoBehaviour someMonoBehaviour;
    
    private void Update()
    {
    if (Input.GetKeyDown(KeyCode.D))
    {
    Destroy(someMonoBehaviour);// 此函数会在走完这一帧之后, 销毁对应C++端的object
    }
    
    if (Input.GetKeyDown(KeyCode.F))
    {
    // 按了D以后再按F, 会抛出异常, 表示object已经被销毁了
    Debug.Log(someMonoBehaviour.enabled);
    }
    
    if (Input.GetKeyDown(KeyCode.G))
    {
    // 按了D以后再按F, 会打印false
    Debug.Log(someMonoBehaviour.someField);
    }
    }
    }

    这段代码会出现不同的结果,就是因为C++这边的对象通过Destroy销毁了,所以enabled属性不存在了,但是由于这里始终记录着someMonoBehaviour在C#端的引用,所以其在C#这边没有被GC掉,所以这一部分的数据,仍然是可以处理的,此时如果在代码监视窗口看someMonoBehaviour对象,打印的还是null,这是因为UnityEngine.Object里override了ToString函数:

    namespace UnityEngine
    {
    public partial class Object
    {
    ...
    
    // Returns the name of the game object.
    public override string ToString()
    {
    return ToString(this);
    }
    }
    
    // ToString是一个C++的dll暴露出来的接口函数
    [FreeFunction("UnityEngineObjectBindings::ToString")]
    extern static string ToString(Object obj);
    
    ...
    }

    参考链接:https://docs.unity3d.com/2022.1/Documentation/Manual/overview-of-dot-net-in-unity.html
    参考链接:http://blog.13pixels.de/2019/how-unity-destroys-objects/
    参考链接:https://forum.unity.com/threads/object-is-null-but-i-can-access-a-property-without-a-null-reference-exception.439090/
    ————————————————
    版权声明:本文为CSDN博主「弹吉他的小刘鸭」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/alexhu2010q/article/details/106695166

  • 相关阅读:
    angular 路由动态加载JS文件
    使用AngularJS处理单选框和复选框的简单方法
    angularjs 请求后端接口请求了两次
    angular ui.router 路由传参数
    linux 安装svn服务器
    gulp css 压缩 合并
    ajax 实现跨域
    [codeforces 1366D] Two Divisors
    Namomo Test Round 1 题解
    Race(淀粉质)
  • 原文地址:https://www.cnblogs.com/TongGeGe/p/16182829.html
Copyright © 2020-2023  润新知