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

相关文章

来自专栏java达人

说说JSON和JSONP( 含jquery例子)

说到AJAX就会不可避免的面临两个问题,第一个是AJAX以何种格式来交换数据?第二个是跨域的需求如何解决?这两个问题目前都有不同的解决方案,比如数据可以用自定义...

2245
来自专栏nice_每一天

[搜索]ElasticSearch Java Api(一) -添加数据创建索引

转载:http://blog.csdn.net/napoay/article/details/51707023

1984
来自专栏大内老A

WCF技术剖析之十九:深度剖析消息编码(Encoding)实现(下篇)

[爱心链接:拯救一个25岁身患急性白血病的女孩[内有苏州电视台经济频道《天天山海经》为此录制的节目视频(苏州话)]]通过上篇的介绍,我们知道了WCF所有与编码与...

2019
来自专栏前端学习心得

Vuex入门到上手

1645
来自专栏CRPER折腾记

2018春招前端面试: 闯关记(精排精校)

box-sizing有两个值:content-box(W3C标准盒模型),border-box(怪异模型),

1132
来自专栏张善友的专栏

MSBUILD 命令行编译的时候请注意msbuild文件名称或路经中空格导致出错

在使用MSBUILD 去编译msbuild文件的时候,如果这个方案或者项目的名称或者路经中间有空格符号,需要把这个方案或者项目整个用引号引起来,否则编译的时候会...

1835
来自专栏程序员宝库

正则表达式实例

来源:寒青 链接:https://segmentfault.com/a/1190000012806098 1. 校验基本日期格式 var reg1 = /^\...

34411
来自专栏大内老A

ASP.NET Core的路由[2]:路由系统的核心对象——Router

ASP.NET Core应用中的路由机制实现在RouterMiddleware中间件中,它的目的在于通过路由解析为请求找到一个匹配的处理器,同时将请求携带的数据...

590
来自专栏何俊林

Android Multimedia框架总结(十二)CodeC部分之OMXCodec与OMX事件回调流程

前言:上篇文中分析到AwesomePlayer到OMX服务,曾介绍到,OMX服务主要完成三个任务: NodeInstance列表的管理,NodeInstance...

25010
来自专栏木宛城主

Unity应用架构设计(6)——设计动态数据集合ObservableList

什么是 『动态数据集合』 ?简而言之,就是当集合添加、删除项目或者重置时,能提供一种通知机制,告诉UI动态更新界面。有经验的程序员脑海里迸出的第一个词就是 O...

1647

扫码关注云+社区