Unity 引擎资源管理代码分析 ( 1 )

一、 简介

目前网络上已经有很多介绍Unity资源管理机制、和API使用方法的文章,但少有文章从Unity源码层面对其实现进行深度解析。作为一名喜欢打破砂锅璺到底程序猿,又有幸在鹅厂接触到Unity官方授权的源代码,实在是克制不住对其实现分析一通。学习收获一二,与众分享,希望能在项目中帮到大家!

本文主要基于Unity 4.6.9的引擎源代码,重点介绍了Unity中GameObject、Asset和Prefab等概念在引擎中的实现,并分析了Resources类和AssetBundle的常用资源加/卸载API的工作机理,及其应用优劣。

二、 资源类型

1. Unity C++ 类图

在分析Unity的资源管理机制之前,我们首先要从Unity引擎的代码层面去理解GameObject、Component、Asset、Prefab等不同类型对象的具体实现,以及它们之间的关系。

其实在Unity引擎的C++源代码实现中,所有的对象都是基于UnityEngine.Object类的。而其中Asset和Prefab只是两个抽象的概念,并没有对应的C++ Class实现。具体见下图:

2. GameObject(橙)、Component(紫)、Asset(红)

在类图中我分别用橙、紫、红三种颜色将我们能在Unity编辑器中直接见到的C++ Class分为了三大类。这些类的属性和方法其实都是由C++代码实现的,只不过暴露给了C#脚本。也就是说在创建这些对象时系统会同时在C#的managed heap和C++ native heap中分配内存。

其中橙色的GameObject类就是我们在编辑器中可创建的对象节点,它本身并不实现任何的渲染或游戏逻辑等功能,即便最基本的空间变换功能也是由默认挂接的Transform组件所实现的。但我们可以在GameObject上挂接MeshRenderer、Animator、SpriteRenderer以及继承于MonoBehavior的自定义脚本组件实现各种各样的渲染及逻辑功能。这些用于实现某个特性功能脚本组件在上图中均被标记为紫色。

这里注意,这些功能组件本身并不包含资源,而只负责实现某个功能。以MeshRenderer组件举例,它通过MeshFilter组件间接获取引用的Mesh模型资源,通过Material对象获取渲染用的材质属性、Shader、以及纹理资源。而像Mesh、Material、Shader、Texture、AnimationController等在上图中标为红色的类则用来保存实际的资源数据,这些类的数据通常都可从文件中读取出来,或者可以被保存为文件。这些对象是货真价实的Assets资源。

3. Prefab

那么Prefab又是什么?我们知道可以将多个GameObject对象挂接为父子级,组成一个完整的场景树。而当我们把其中的一部分子树在Unity编辑器中拖拽到资源视图中时就会生成一个对应的.prefab文件。这个.prefab文件中保存的就是这个场景子树中包含的所有GameObject,这些GameObject下挂接的组件、属性、及对资源的引用关系。

当我们通过Resources.Load之类的接口加载.prefab文件时,引擎则会自动创建这些GameObject、Component,加载其所引用的资源,并恢复其组织关系。保存时则反之。但注意,由于组件并不实际保存资源数据,因此.prefab文件也并不直接保存其引用的任何资源数据。取而代之,.prefab文件通过一个guid来索引其引用到的资源。

下图为保存了一个人物模型的prefab文件和这个模型fbx对应的meta文件内容的部分截图。可见prefab文件中mesh和avatar记录的guid都跟meta文件中的guid一致,也就是说其中Animator组件引用的avatar资源和SkinMeshRenderer组件引用的mesh资源都是从这个fbx文件中加载的。

三、 资源管理API分析

1. Resources.Load

为了方便理解Unity引擎的工作机制、避免AssetBundle等资源打包机制造成干扰,我们从最原始、最直接的Resources.Load接口开始分析资源加载流程。但由于版权问题下文不会直接贴出Unity引擎源代码,只会对执行流程作大概的解释,敬请谅解。

在Unity引擎中,Resources.Load接口对应的C++函数为Resources_CUSTOM_Load。该函数做的第一件事是在ResourceManager.GetPathRange函数中根据传入的资源路径字符串在一个std::multimap<UnityStr, PPtr<Object>>类型的map中查找资源对象的指针。在不考虑AssetBundle的情况下,只有Assets/Resources/目录下的资源会被预先索引到这个表中。

这个查找过程有三个地方值得注意:

第一,路径中所有的字符会首先被转换为小写,且所有的目录分隔符都使用“/”,并去除了扩展名。也就是说当资源路径只有大小写或扩展名区分的时候,对Unity来说这两个路径是没有区别的。

第二,这个Unity自己定义的PPtr类其实并没有存储Object指针的成员变量,它实际上只存储了一个int类型的InstanceID,但是它重载了所有对指针进行访问的操作符。当访问对象指针时,它会通过Object::IDToPointer函数在一个全局对象表中查找实际的对象指针,并返回。

第三,这个查找表是一个std::multimap类型的容器,也就是说它是允许使用相同路径的键值存储多个对象。也就是说假设在相同目录下,除了存在我们需要加载的foo.prefab外,还有个foo.shader的资源时,引擎只会加载multimap中同名的第一个资源。如果我们在调用Resources.Load接口指定了第二个对象类型的参数,Unity引擎则会在加载完对象后去判断这个对象的类型与我们指定的类型是否相同(或是否为其子类),如果相同则break跳出循环,不加载其后的对象。因此我强烈建议大家不要让资源的命名重复,或在加载资源时不指定具体的类型。这不但会造成多余的资源加载操作,还有可能造成资源类型转换错误。

对于有兴趣阅读Unity源代码的同学,这里我要多提个醒,Unity的Object对象创建及数据读取代码是隐藏在PPtr<T>::operatorT* () const这个操作符重载函数里的,也就是说你看到第一行尝试对Object指针进行访问的代码即是实际对象加载的位置。其反序列化的内部函数为PersistentManager::ReadObject。我第一次跟代码的时候也一不小心就F10过去了……

例如我们要加载一个foo.prefab这个文件,这个文件中包含三个GameObject:A、B、C,其中GameObject B、C下各挂接了自己的MeshRenderer和MeshFilter组件,并引用了自己的Mesh、Material资源,共享了一个Shader资源。如下图:

当PersistentManager::ReadObject函数加载完这个foo.prefab中的根级GameObject A之后,它会调用这个对象的CheckConsistency函数,这个函数是Object基类的虚函数,负责检查在该对象中包含的所有可永续化的(代码原文Persistent,直白的说就是可通过文件存取。)成员变量的正确性。无论GameObject还是Component,所有继承自Object的子类都必须实现。于是乎,当GameObject - A检查它包含的Component的时候,发现其下的Transform组件又引用了GameObject B和C,则会去获取GameObject B和C的指针。从而无可避免地又通过PersistentManager::ReadObject函数去加载GameObject B、C。加载B、C的时候又引用到了他们的MeshFilter和MeshRenderer组件,从而进一步加载了它们的Mesh和Material资源。当首先加载Material B的时候,由于Shader B&C尚未加载,因此会自动加载它。而当它再次加载Material C的时候,由于发现Shader B&C已在InstanceID to Pointer的全局表中存在了,所以就直接引用它,而不会重复加载。举一反三,所有Prefab的节点树及其组件和资源就是按照这样的方式被加载完成的。

(精彩待续……写文档真是个体力活,后续内容还未整理完毕。大家多多支持,我会再接再厉的!)

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏UE4技术专场

UE4 ReplicationGraph分析

ReplicationDriverClassName="/Script/ProjectName.ClassName"

2162
来自专栏Aloys的开发之路

一个比较全面的java随机数据生成工具包

        最近,由于一个项目的原因需要使用一些随机数据做测试,于是写了一个随机数据生成工具,ExtraRanom。可以看成是Java官方Random类的扩...

2229
来自专栏猿人谷

memcpy和memmove的区别

memcpy()和memmove()都是C语言中的库函数,在头文件string.h中,其原型分别如下: void *memcpy(void *dst, con...

2155
来自专栏程序员的诗和远方

30分钟QUnit入门教程

30分钟让你了解Javascript单元测试框架QUnit,并能在程序中使用。 QUnit是什么 QUnit是一个强大,易用的JavaScript单元测试框架,...

3629
来自专栏数据库

《数据库系统概念》15-可扩展动态散列

静态散列要求桶的数目始终固定,那么在确定桶数目和选择散列函数时,如果桶数目过小,随着数据量增加,性能会降低;如果留一定余量,又会带来空间的浪费;或者定期重组散列...

2287
来自专栏生信宝典

R语言学习 - 箱线图一步法

箱线图 - 一步绘制 绘图时通常会碰到两个头疼的问题: 有时需要绘制很多的图,唯一的不同就是输入文件,其它都不需要修改。如果用R脚本,需要反复替换文件名,繁琐又...

2995
来自专栏吾爱乐享

白盒测试的测试方法及基本路径测试法

1283
来自专栏为数不多的Android技巧

ASCII Art:使用纯文本流程图

我们使用纯文本写代码,有了Markdown又可以使用纯文本写文档,那么对于更直观的信息表达方式——图片,能不能使用纯文本描述呢?

1352
来自专栏奇点大数据

【干货】Pytorch中的DataLoader的相关记录

DataLoader简单介绍 DataLoader是Pytorch中用来处理模型输入数据的一个工具类。通过使用DataLoader,我们可以方便地对数据进行...

9856
来自专栏java系列博客

UML——序列图

1974

扫码关注云+社区