找回密码
 立即注册
查看: 345|回复: 0

1.8 从0开始学习Unity游戏开发--编写物体组件

[复制链接]
发表于 2023-4-1 05:23 | 显示全部楼层 |阅读模式
上篇文章我们初步的接触了怎么创建游戏物体的功能组件代码,这篇文章我们将会详细的讲解这个代码是怎么被Unity使用的,以及我们如何编写在Inspector上可以展示参数的功能组件。
Unity反射调用声明周期函数

在上一章中,我们先硬灌输了一个Start函数会被Unity在适当的时机调用的知识,但是这并不符合我们的常识,一个函数被调用,肯定有类似这样一个代码:
classA.Start();
但是Unity中我们的组件类里面却并不需要这样的代码就能被Unity调用,甚至这个Start函数还是private访问控制的,正常手段根本无法从外部调用。
所以Unity使用的是C#语言的特性:反射(Reflection)。
听起来好像很厉害,其实就是编译器记录了你编写的代码的内容,然后代码可以通过反射这样一个功能知道你写了哪些函数,哪些变量,甚至调用函数,改变变量的值等操作。
是不是还没懂?没事,我们手动来实现一下,因为涉及到指针,C#不太直观,我们换回C++来做示例:
class Test
{
private:
    void Func() {}
};

Test t;
t.Func();  // <-- 错误,你无法调用到private下的成员函数
这里我们新定义一个Test类,给了一个Func成员函数,但是是private访问控制的,所以正常情况,我们无法通过Test的实例调用到这个Func函数。
另一方面,如果了解了函数指针,我们是能知道类的成员函数也是有指针的,所以我们只需要将类的成员函数作为指针保存在一个我们够得到的地方,这样我们就可以调用了,让我们来一些魔法:
#include <iostream>

class Test
{
public:
    // 为了方便,我们将Func函数指针的类型换个名字叫MemberFunctionPointerType
    typedef void (Test::*MemberFunctionPointerType)();
    // 定义一个public函数来获取私有的成员函数指针
    MemberFunctionPointerType GetFuncPointer() { return &Test::Func; }
private:
    void Func() { std::cout << "Hello world!"; }
};

int main() {
    Test t;
    auto memberFunc = t.GetFuncPointer();
    // 调用获取到的私有函数指针
    (t.*memberFunc)();
    return 0;
}
运行这个代码,你可以看到输出了Hello world!如果不熟悉函数指针的话,可以再去学习一下基础。
可以看到通过一些魔法,我们成功调用到了本来不可能访问的函数,并且这个函数也无法找到正常途径下的任何调用引用(IDE上查引用是查不到的)。
而C#则直接内置了这样一套机制,编译器直接生成了这些东西,并最终构建到你的程序中,并且不仅仅是调用函数,可以操作和访问的东西要多得多。
例如官方示例里面有这样一段代码:
// Using GetType to obtain type information:
int i = 42;
Type type = i.GetType();
Console.WriteLine(type);
对于任何C#的类型,包括内置的类型int之类的,都可以通过GetType()拿到这个类型的信息,而通过Type类,则可以获取到这个类型代码定义的各种东西,包括有哪些函数,哪些成员,然后还能调用函数。
那么到了Unity中,Unity则是通过在游戏运行开始的时候,将GameObject所持有的组件,或者是我们写的MonoBehaviour类收集起来,然后通过反射,获取到我们所有写在代码里面所关心的回调函数,然后在对应时机,使用组件类的实例+反射得到的函数信息来调用你所写的逻辑。
如果实际实现一下Unity做法就是类似这样:
// 从GameObject中拿到每个组件,其中就有我们的DemoTest类
Monobehaviour behaviour = demoTest;

// 反射得到我们写的DemoTest的类型信息
Type type = behaviour.GetType();

// 然后找一下Start方法
MethodInfo startFunction = type.GetMethod("Start", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

// 然后在适当的时机调用Start方法,第一个参数是调用哪个实例的方法,第二个是参数是方法的参数数组
startFunction.Invoke(demoTest, null);
如此以来就解释了为什么我们看不到任何代码调用了我们的Start方法,但是却真的被调用了的疑问。
除了Start方法,还有很多其他的方法可以被调用,完整的列表可以参考官方文档的这张图:


Unity展示组件可编辑参数

在之前的文章中,我们看到了Cube自带的一些组件都是有输入框能修改参数的,例如Transform组件就可以修改位置的XYZ三个坐标的数值,那么如果我们的组件需要加一些参数给到编辑器呈现出来修改的话应该如何做呢?
其实很简单,根据上面讲的,Unity会通过反射拿到我们写的代码里面都有一些什么成员变量和函数,那么Unity也会使用一样的方法获取到我们想要展现的参数到编辑器上以供修改。
成员变量满足如下任何一条就会被Unity通过反射收集:

  • 访问控制是public
  • 访问控制不是public,但是加了[SerializeField]这样一个属性,例如这样:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DemoTest : MonoBehaviour
{
    public int testValue1;
   
    [SerializeField]
    private int testValue2;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Hello World!");
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
改完让Unity自动编译后,可以再次选中Cube观察Inspector面板来看:


可以看到我们的两个变量都上去了,并且Unity还帮我们做了显示名称的优化。
但是值得注意的是,并不是所有类型的数据都可以支持在编辑器上显示,为了支持显示,数据类型必须是可序列化的,Unity默认支持了很多类型的序列化操作,但是对于我们自定义的类则默认不支持,如果想要Unity支持,则需要对对应的类的定义加上可序列化的属性:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DemoTest : MonoBehaviour
{
    public int testValue1;
   
    [SerializeField]
    private int testValue2;

    [System.Serializable]   // <-- 这一行是关键,必须有这个属性,否则Unity是无法识别这个类型数据的
    public class CustomDataType
    {
        public int nestValue;
    }

    public CustomDataType testValue3;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Hello World!");
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
可以看到如果我们的成员是自定义类型,那么这个自定义类型一定需要加上[System.Serializable]属性,否则不会被Unity认,修改后效果就是这样:


现在可以任意调整Cube这个DemoTest的几个参数的值,那么这个时候其实你会有进一步的问题,我改的值都存哪里去了?
Unity的资源序列化

接上面的问题,说到序列化,那么可以很直观的想到,我们不仅是修改组件上的参数,还包括我们直接在Scene里面搭建场景,新建GameObject,都其实是在产生新的数据,那么数据序列化落地,肯定是需要落地到文件上,而我们从头到尾都只创建了一个场景的资源文件叫Demo,所以很顺理成章的可以认为我们是将数据都保存到了Demo这个场景资源文件内。
很正确,我们先Ctrl+S保存一下我们目前的修改,然后直接在资源管理器里面(Mac上应该是访达里面)找到这个Demo文件:


这个文件其实是一个文本文件,我们可以使用任何文本编辑器打开它,这里我用vscode打开:


一开头我们就能看到这个文件其实就是YAML格式的文件,我们可以搜一下我们在场景里面创建的唯一一个叫Cube的物体就可以找到它:


是不是很熟悉,这个Cube是一个GameObject,然后GameObject里面存有一个m_Component数组,用来存储在这个GameObject上的组件,数一下有五个,其他的还有m_Name的值等等,接下来还有BoxCollider组件的具体细节等等,然后我们看看我们自己写的DemoTest组件:


如果我们没有修改组件上的值,其实我们看不到那几个变量,因为还是默认值,我们手动改一下Inspector上值保存再看看:




可以看到出现了我们在编辑器上修改的值,那么这里其实就能看到Unity其实是通过反射,获取到我们希望将哪些变量序列化,而支持序列化的内容,将会默认被展示在Inspector上,变量对应的值,则被序列化到具体的资源文件中,例如场景内的所有内容都会被序列化到场景所在的资源文件中(当然后面会讲预制体,会有所例外)。
下一章

本章我们详细讲解了Unity是如何组织GameObject最重要的组件内容,如何调用我们写的代码,如何序列化配置的数据,通过这些将会对Unity如何组织整个场景数据有一个比较清晰的认知。
下一章,我们将会稍微轻松一点,讲讲我们的游戏如何打包发布,这步虽然就是点点点就能完成,但是却是我们能发布给别人体验的重要步骤。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 21:36 , Processed in 0.133571 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表