Assets和Objects

Asset是存储在硬盘上的文件,保存在Unity项目的Assets文件夹内。比如:纹理贴图、材质和FBX都是Assets。一些Assets以Unity原生格式保存数据,例如材质。另一些Assets需要通过处理转换到原生格式,例如FBX。

Object是一系列序列化数据,这些数据描述了具体的资源实例,这可以是Unity使用的任意类型的资源,例如mesh,sprite,audio clip或animation clip。所有的Objects都是UnityEngine.Object的子类。

大部分Object类型都是Unity内置的,但有两个特殊类型:

1. ScriptableObject允许开发者定义他们自己的数据类型。这些类型能够由Unity序列化和反序列化,并且在编辑器的Inspector窗口中进行操作。

2. MonoBehaviour提供了链接到MonoScript的封装。MonoScript是Unity的内部数据类型,其中保存了指向在具体的程序集和命名空间中的具体脚本类的引用。MonoScripte不包含任何实际可执行的代码。

Assets和Objects之间存在一对多的关系:也就是说,Asset文件内能够包含一个或多个Objects。

内部对象引用

所有的UnityEngine.Objects都可以引用其他的UnityEngine.Objects,被引用的Objects可以和引用的Objects位于同一个Asset文件内,也可以是由其他Asset文件导入的。例如,材质对象通常有一个或多个纹理对象的引用,这些纹理对象通常都是从纹理资源文件导入的(例如PNG或JPG)。

当序列化的时候,这些对象由两部分分离的数据组成:文件的GUID和Local ID。文件的GUID标记了存储资源的Asset文件。Local ID是局部唯一的(也就是说,在每个Asset文件中,Local ID都是唯一的),标记了Asset文件中的每个Object。

文件的GUID存储在.meta文件中。这些.meta文件是Unity第一次导入Assets时生成的,并且和Asset存储在同一个目录中。下图展示了Diffuse材质及其.meta文件:

.meta文件中包含了GUID:

打开材质文件本身,可以看到Local ID:

如果在场景中有对象使用该材质进行渲染,那么打开场景文件后,就会发现该材质对象由GUID以及Local ID来标记:

为什么使用GUID和Local ID?

GUID的功能是提供文件路径的抽象表示。只要使用GUID来关联具体的文件,那么文件在磁盘上的位置就无关紧要了。因此可以随意移动文件而不需要更新引用该文件的Objects(因为这些Objects存储的都是文件的GUID)。

由于一个Asset文件可能包含多个UnityEngine.Object资源,因此需要用Local ID来明确的标记每个不同的Object。

如果一个Asset文件关联的GUID丢失的话,那么所有对该Asset文件中的Objects的引用都将丢失。当.meta文件丢失时,Unity会重新生成。

Unity维护了具体文件路径与GUID的映射关系。当一个Asset被加载或导入时,就会新增一个映射项,该映射项将Asset的文件路径和Asset文件的GUID连接在一起。如果一个Asset的.meta文件丢失但其文件路径没有发生变化的话,Unity能确保重新生成的.meta中记录的GUID是保持不变的。

如果.meta文件在Unity关闭时丢失,或者Asset文件的路径发生了变化,但.meta文件没有跟着一起移动的话,那么所有对该Asset文件中的Objects的引用都将丢失。举个例子,场景中的Cube使用了我创建的材质Diffuse:

Diffuse材质及其.meta文件存储在Assets目录下,如果现在在外部移动Diffuse材质到Assets/Temp目录下,由于没有同时移动其.meta文件,因此Cube对其引用就会丢失:

资源及其导入

非Unity原生资源必须导入进Unity中才能使用,这是通过asset importer完成的。这些improter在资源导入时会被自动调用,同时你也可以用AssetImporter及其子类的API来通过代码调整资源导入过程。

资源导入的结果是一个或多个UnityEngine.Objects。在Unity中你可以看到一个父对象包含多个子对象,例如sprite atlas:。这些对象都共享同一个GUID,因为他们的源数据来自于同一个Asset文件。Unity使用Local ID来区分他们:

资源导入过程包含了十分耗时的操作,例如纹理压缩。所以如果每次打开Unity都需要执行一遍资源导入过程的话将会十分低效,因此,Unity将资源导入的结果缓存在Library文件夹中:。具体来说,存储在以Asset文件的GUID前两个数字命名的文件夹中,这些文件夹位于目录Library/metadata:

实际上即使是Unity原生资源,也会将导入结果存储在对应文件中。但是原生资源不需要很长的转换时间或重新序列化时间。

实例ID

尽管GUID和Local ID健壮耐用,但是GUID的比较很耗时,而在运行时我们需要有个十分高效的系统。因此Unity在内部会维护一份缓存,这份缓存将GUID和Local ID转换成独一无二的整数,这些整数被称为Instance ID,每当有新的Objects添加到缓存中时,Instance ID以简单的单调递增的方式进行赋值。缓存维护了Instance ID,GUID和Local ID(这两个定义了Object的源数据在磁盘上的位置)以及Object在内存中的实例(如果Object已经被加载到内存中的话)之间的映射关系。这样UnityEngine.Objects就可以维护相互之间的引用关系。通过Instance ID可以快速找到对应的已经加载的Object,如果对应的Object还没有加载,那么就可以通过GUID和Local ID来找到Object的源数据,然后加载相应的Object。

应用程序启动时,项目内置对象(比如场景中使用的对象)的数据以及在Resources文件夹中的对象的数据将被初始化到Instance ID缓存中。当运行时有新的资源被导入(比如通过脚本创建的Texture2D对象),以及当从AssetBundle中加载对象时,就会在缓存中添加Instance ID项。Instance ID只有在被认为已经过时的情况下才会从缓存中删除,这种情况发生在一个AssetBundle被卸载时。当一个AssetBundle被卸载时,除了会导致对应的Instance ID被认为已经过时,Instance ID和GUID以及Local ID之间的映射数据也会被从内存中删除。如果AssetBundle被重新加载的话,那么从该AssetBundle中加载的每一个对象都会创建一个新的Instance ID。

需要注意的是在具体平台上的一些特定事件会导致Objects从内存中被删除。比如当iOS上的应用程序被挂起时,图形资源可能会从显存中被删除,如果这些资源是来自一个已经被卸载的AssetBundle,那么Unity就无法重新加载这些资源了,任何对这些资源的引用也将变得无效(例如出现不可见的模型(missing)使用粉色的材质(missing)来渲染)。

MonoScript

一个MonoBehaviour包含了一个对MonoScript的引用,而MonoScript仅仅包含了用于定位到一个具体脚本类所需的信息,他们都不包含脚本类的可执行代码。

一个MonoScript中包含了三个字符串:一个程序集名,一个类名以及一个命名空间名。

当Unity构建项目时,会将Assets文件夹下的所有脚本文件编译到Mono程序集中。具体来说,Unity会为在Assets文件夹中使用的每种不同的编程语言编译一个程序集,并且会将在Assets/Plugins文件夹中的脚本单独编译到一个程序集中。在Assets/Plugins文件夹外的C#脚本会被编译到Assetmbly-CSharp.dll中,在Assets/Plugins文件夹外的Java脚本会被编译到Assembly-UnityScript.dll中,Assets/Plugins中的脚本会被编译到Assembly-CSharp-firstpass.dll中。

这些程序集(再加上预编译的程序集)都会被包含在最终的应用程序中:

这些程序集就是MonoScript引用的程序集。和其他资源不同,所有程序集在应用程序第一次启动时会被全部加载进来。这种方式也是为什么一个AssetBundle(或者一个Scene、一个Prefab)中不包含挂载的MonoBehaviour组件中的可执行代码。这种方式使得不同的MonoBehaviour可以引用共同的具体类。

资源生命周期

有两种加载UnityEngine.Objects的方式:自动加载和显示的手动加载。当一个Instance ID被解引用,其对应的Object当前没有加载到内存中,并且Object的源数据能够被定位到时,Object会被自动加载。Objects还能够显示的在脚本中手动加载,例如新建一个Texture2D或通过AssetBundle.LoadAsset方式加载一个Object。

如果一个文件GUID和Local ID没有对应的Instance ID,或者一个Instance ID对应的Object没有被加载,并且其对应的GUID和Local ID是无效的话,那么Object就不会被加载,但是引用关系仍旧会被保留,此时在Unity编辑器中就会出现"(Missing)"。

Objects在下面三种具体的情况下会被卸载:

1. 当清理未被引用的Asset时,未被引用的Objects会被自动卸载。当场景切换时或当调用Resources.UnloadUnusedAssets函数时会触发清理未被引用的Asset。

2. 来自Resources文件夹的Objects在调用Resources.UnloadAsset函数时会被销毁。但是Instance ID会被保留,所以如果在Object被销毁后,有任何先前对该对象的引用被解引用时,Unity会重新通过Instance ID找到GUID和Local ID,然后将该对象再次加载进来。

3. 来自AssetBundle的Objects在调用AssetBundle.Unload(true)函数时会被立即销毁,同时也会使得Instance ID,GUID和Local ID变得无效,任何对该对象的引用也会变成"(Missing)"。之后在C#中任何对该对象的访问都会引发"NullReferenceException"异常。如果调用AssetBundle.Unload(false),从AssetBundle加载的Objects不会被销毁,但是Instance ID对应的GUID和Local ID会变得无效,因此如果这些对象被从内存中释放的话,Unity将无法再次加载他们。

加载大层级对象

当序列化Unity GameObjects(例如Prefabs)时,要记住整个层级都会被序列化。也就是说,层级中每个GameObject及其组件在序列化数据中都会被独立的表示。因此,加载和实例化具有大层级的GameObjects时会有性能影响。

当实例化GameObjects时,实例化一个具有大层级的GameObject和实例化多个小层级的GameObjects然后将这些GameObjects组合在一起相比,需要耗费更多的CPU时间。尽管实例化一个大层级的GameObject不需要组合GameObjects(不需要trampolining和SendTransformChanged回调)的CPU时间,但这些节约的CPU时间远远比不过读取和反实例化大层级数据的时间。

之前提到,序列化GameObjects时,整个层级中的GameObject及其组件数据都会被序列化 --- 即使这些数据是重复的。比如一个UI中有30个一样的Button,那么Button数据会被序列化30次。在加载时,这些数据都需要从磁盘上进行读取,在加载大层级的GameObjects时,文件读取时间会消耗大量CPU时间。因此,可以把重复对象从整个层级中移出来,再单独实例化后再组合到整个层级中。

Unity学习笔记 - Assets, Objects and Serialization的更多相关文章

  1. 微软企业库Unity学习笔记

    本文主要介绍: 关于Unity container配置,注册映射关系.类型,单实例.已存在对象和指出一些container的基本配置,这只是我关于Unity的学习心得和笔记,希望能够大家多交流相互学习 ...

  2. Unity学习笔记(一)——基本概念之场景(Scene)

    场景,顾名思义就是我们在游戏中所看到的物品.建筑.人物.背景.声音.特效等,基本上和我们玩游戏时所看到的游戏“场景”是同一个概念. Unity 3D中,“场景”是一个视图,我们通过“场景”这个视图,来 ...

  3. Unity学习笔记(二)——第一个Unity项目Hello Unity

    保留版权,转载请注明出处:http://blog.csdn.net/panjunbiao/article/details/9318811 在这一篇文章里,参照宣雨松的<Unity 3D游戏开发& ...

  4. Unity学习笔记(5):动态加载Prefab

    第一种方法,从Resources文件夹读取Prefab Assets/Resources文件夹是Unity中的一个特殊文件夹,在博主当前的认知里,放在这个文件夹里的Prefab可以被代码动态加载 直接 ...

  5. Unity学习笔记

    『 知识点』 [射线] 射线检测碰撞 『游戏实战』 个例 [E]<愤怒的小鸟> 资源 免费Unity基础教程(中文电子书) [E] noobtus(Unity游戏教程)

  6. Unity学习笔记(4):依赖注入

    Unity具体实现依赖注入包含构造函数注入.属性注入.方法注入,所谓注入相当赋值,下面一个一个来介绍 1:构造函数注入 1.1当类有多个构造函数时,可以通过InjectionConstructor特性 ...

  7. Unity学习笔记(3):获取对象

    在上一篇文章中(Unity映射注册)中概要介绍了Unity中的映射机制,本节主要介绍对象获取,包括默认获取,通过名称获取,获取全部对象,同时通过加载配置文件,然后再获取对象. 通过代码获取对象 方式1 ...

  8. Unity学习笔记(2):注册映射

    在上一篇文章中(认识Unity)中概要介绍了Unity和Ioc,本节主要介绍IoC中的注册映射,并使用代码和配置文件两种方式进行说明. 定义依赖注入相关信息 定义ILogger接口 public in ...

  9. Unity学习笔记(1):认识Unity

    Unity是什么? Unity是patterns & practices团队开发的一个轻量级.可扩展的依赖注入容器,具有如下的特性: 它提供了创建(或者装配)对象实例的机制,而这些对象实例可能 ...

随机推荐

  1. python基础-文件处理与函数

    1. 文件处理 1.1 文件处理流程 1.打开文件,得到文件句柄并赋值给一个变量 2.通过句柄对文件进行操作 3.关闭文件 1.2 文件读取模式r r文本模式的读,在文件不存在,不会创建新文件 f = ...

  2. Scala实战高手****第16课:Scala implicits编程彻底实战及Spark源码鉴赏

    隐式转换:当某个类没有具体的方法时,可以在该类的伴生对象或上下文中查找是否存在隐式转换,将其转换为可以调用该方法的类,通过代码简单的描述下 一:隐式转换 1.定义类Man class Man(val ...

  3. linux-去重-uniq

    uniq : 默认(去重)  |  -d(显重)   |   -u(删重) 语法:uniq  [选项]  文件 选项 -c或--count 在每列旁边显示该行重复出现的次数 -d或--repeat 仅 ...

  4. Word中插入带公式的Visio注意事项

    有时候发现,有的公式显示的间距特别大,那么在word中右键打开Visio,改好后,保存了,word里还是那样. 因为你需要吧改好的另存为原来的visio文件(名字.位置要一样,就是说替换原来的文件), ...

  5. 请用心练完这16个webpack小例子

    16个demo,webpack+react搭配使用首先教大家2个新技能 1.按照正常github地址情况下,你的github本身不能访问目录. 例如要访问vue-demo下的vueCpu文件夹:htt ...

  6. sersync部署

    rsync :  wget  http://rsync.samba.org/ftp/rsync/src/rsync-3.1.1.tar.gz Sersync: wget https://raw.git ...

  7. jenkins如何在一台机器上开启多个slave

    1.一台机器不是jenkins的master分支 2.另一台机器部署多个slave分支 3.部署多台slave分支的机器其实只需要在多个目录放置多个slave.jar就可以了,然后进行一些配置即可

  8. javascript快速入门11--正则表达式

    正则表达式可以: 测试字符串的某个模式.例如,可以对一个输入字符串进行测试,看在该字符串是否存在一个电话号码模式或一个信用卡号码模式.这称为数据有效性验证 替换文本.可以在文档中使用一个正则表达式来标 ...

  9. C#秘密武器之LINQ to SQL

    LINQ to SQL语句(1)之Where 适用场景:实现过滤,查询等功能. 说明:与SQL命令中的Where作用相似,都是起到范围限定也就是过滤作用的,而判断条件就是它后面所接的子句.Where操 ...

  10. Selenium webdriver Java 封装与重用

    目的 1. 简化调用 WebDriver对页面的操作,需要找到一个WebElement,然后再对其进行操作,比较繁琐: WebElement element =driver.findElement(B ...