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 条评论
登录 后参与评论

相关文章

来自专栏我是攻城师

分布式日志收集之Logstash 笔记(二)

2826
来自专栏逸鹏说道

Python3 与 C# 扩展之~装饰器专栏

终于期末考试结束了,聪明的小明同学现在当然是美滋滋的过暑假了,左手一只瓜,右手一本书~正在给老乡小张同学拓展他研究多日的知识点

694
来自专栏xingoo, 一个梦想做发明家的程序员

[Logstash-input-redis] 使用详解

redis插件的完整配置 input { redis { batch_count => 1 #返回的事件数量,此属性仅在list模式下起...

26810
来自专栏编码小白

FreeMarker与JSP 2.0 + JSTL组合进行比较

FreeMarker与JSP 2.0 + JSTL组合进行比较。 FreeMarker优点: FreeMarker不受Servlet或网络/ Web的限制; ...

5014
来自专栏Crossin的编程教室

像对象一样对待数据

咱们编程教室有不少同学,学完了基础课程,掌握了一定的编程能力,开始做项目了。然后很可能遇到一个问题:管理数据。课程里有讲过用文件保存数据,还有 pickle、c...

702
来自专栏青枫的专栏

传智播客_毕姥爷_2012年毕向东Java基础教程_毕向东老师

视频百度网盘下载链接:https://pan.baidu.com/s/1bpD3P07#list/path=%2F

351
来自专栏DannyHoo的专栏

iOS中解决后台返回的null导致的崩溃问题--NullSafe

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

643
来自专栏Python专栏

你真的会用python写mongodb的URI吗?

1143
来自专栏racaljk

《代码整洁之道》摘录总结

1.     以下全部条款源于·<Clean Code Robert.C.Martin>Chapter 17,这里对其进行文字层面的加工,简化,便于以后能短时浏...

623
来自专栏企鹅号快讯

Node篇 3.NodeJS整合MySQL

我们在上一篇《[JavaScript从入门到放弃] Node篇 2.Express路由分离及传参》简单的学习了设置路由以及获取参数的几种方式,但显然我们只能利用...

2169

扫码关注云+社区