小言_互联网的博客

Unity对象和序列化原理介绍

324人阅读  评论(0)

Unity使用了很多年了,在这里我敢说很多人对Unity对象和序列化原理并不是很清楚,比如Asset与Object之间有啥区别?如何管理Asset?Unity的序列化是咋回事等等,还有在项目中遇到资源丢失,它为什么丢失了等等,本篇就给读者介绍Unity对象的内部运行原理,下面就从Asset与Object对象区别说起。

Asset与对象

先介绍Asset,它是存储在Unity项目中的文件夹中,比如纹理,3D模型,音频文件等等。Unity对象也就是我们说的Object,它是一组序列化的数据,比如Mesh,Sprite,AudioClip或者AnimationClip,所有对象都是Object基类的子类。
Asset与对象之间是一种一对多的关系,换句话说,任何给定的Asset文件都包含一个或多个Objects。
继续介绍,我们知道对象之间可以互相引用,引用的对象可以保存在同一个Asset文件中,也可以从其他Asset文件导入,比如:材质 Object 通常具有一个或多个对纹理对象的引用,这些纹理对象通常从一个或多个纹理资源文件导入。
序列化时,这些引用由两个单独的数据组成:文件GUID 和 本地ID,文件GUID 标识存储目标资源的资源文件。本地惟一的(引用)ID标识资源文件中的每个对象,因为一个资源文件可能包含多个对象。文件GUID 存储在 .meta 文件中。这些 .meta 文件是在 Unity 首次导入资源时生成的,并存储在与资源相同的目录中。如下图所示:

再看一下对应的GUID标识,如下图所示:

图中guid即是GUID的标识,如果想看一下ID的标识,我们打开一个mat文件,如下图所示:

在上面的示例中,以&符号开头的数字是对象的本地ID,如果 Object 位于由文件GUID“f0d883fa…”标识的Asset内,则可以将 Object 唯一标识为文件GUID“f0d883fa…” 和本地ID“2100000” 的组合。
为什么 Unity 的文件GUID 和本地ID 是必需的?答案是可以让资源和对象独立于平台。
GUID 标识提供了特定位置的抽象,只要文件标识GUID 可以与文件相关联,该文件在磁盘上的位置就变得无关紧要了,该文件可以自由移动,而无需更新引用该文件的所有对象。这个就好比,我们Unity工程中的文件可以随意移动位置都没有关系。
由于Assets中包含多个Object,我们需要ID对她们加以区分,如果与Asset文件关联的文件GUID 丢失,则对该Asset文件中所有对象的引用也将丢失,这就是为什么重要的是 .meta 文件必须保持与相关文件名相同的文件名字,并与相关的Asset文件保存在同一文件夹中,请注意,如果Asset放错位置,Unity会重新生成新的 .meta 文件。

Unity Editor 具有指向已知文件GUID 的文件路径映射,只要加载或导入Asset,就会记录该映射,映射将Asset的路径链接到 GUID。如果在 .meta 文件丢失且Asset路径未更改时, Unity 编辑器处于打开状态,则编辑器可以确保Asset保留相同的文件GUID。
如果在关闭 Unity 编辑器时丢失 .meta 文件,或者Asset的路径发生更改而 .meta 文件并没有随Asset一起移动时,那么对该资产中对象的所有引用都将被破坏。我们在上传资源到服务器时,经常遇到引用丢失就是因为meta更改了或者丢失了。

资源导入

导入过程的结果是一个或多个 UnityEngine.Objects,例如嵌套在Asset下的多个Sprite已导入为Sprite图集。这些对象中的每一个都将共享文件GUID,因为它们的源数据存储在同一Asset文件中。它们将通过本地ID在导入的纹理 Asset 中区分。

导入过程将源资源转换为适合 Unity Editor 中选择的目标平台(如 windows)的格式。导入过程可以包括许多其他特殊操作,例如纹理压缩,由于这通常是一个耗时的过程,因此导入的Asset会缓存在 Library 文件夹中,从而无需在下次编辑器启动时再次重新导入 Assets。

具体来说,导入过程的结果存储在以 Asset 的文件GUID 的前两位数命名的文件夹,该文件夹存储在 Library / metadata / 文件夹中,Asset中的各个对象被序列化为单个二进制文件,其名称与Asset的文件GUID 相同。如下图所示:

序列化和实例

Unity 内部维护着一个缓存,在向缓存注册新对象时以递增的顺序分配实例ID,缓存维护实例ID、文件GUID 和定义对象之间的映射,以及对象在内存中的实例。这允许Objects 可以维护对彼此的引用,在解析实例ID引用时,可以快速返回由实例ID 表示的已加载对象,如果没有加载目标对象,则可以将文件GUID 和本地ID 解析为对象的源数据,从而允许 Unity 即时加载对象。
程序在启动时,实例ID 缓存初始化项目所需要的所有对象的数据,包括Resources中和导入的Asset资源。仅当卸载对文对象时,才会从缓存中删除实例ID。发生这种情况时,将删除实例ID和对应的文件GUID 和本地ID 之间的映射以节省内存。如果重新加载 AssetBundle,将为从重新加载的 AssetBundle 加载的每个 Object 创建一个新的实例ID。资源文件GUID 不能在运行时查询。

MonoScripts

MonoScript 包含三个字符串:程序集名称,类名称和命名空间,构建项目时,Unity 会将 Assets 文件夹中的所有松散脚本文件编译为 Mono 程序集,插件子文件夹之外的 C#脚本放在 Assembly-CSharp.dll 中,Plugins 子文件夹中的脚本放在 Assembly-CSharp-firstpass.dll 中,依此类推,此外,Unity 2017.3 后的版本还引入了定义自定义托管程序集的功能。

这些程序集以及预构建的程序集DLL 文件都包含在 Unity 应用程序中,它们也是 MonoScript 引用的程序集,与其他资源不同,Unity 应用程序中包含的所有程序集都在应用程序启动时加载。

资源生命周期

要减少加载时间并管理应用程序的内存占用,理解 UnityEngine.Objects 的资源生命周期非常重要。加载对象时,Unity 会尝试通过将每个引用的文件GUID 和本地ID 转换为实例ID ,加载对象接口比如:AssetBundle.LoadAsset。加载的对象分两种:
一是,实例ID 引用当前未加载的 Object。
二是,实例ID 有在缓存中注册的有效文件GUID 和本地ID。
如果文件GUID 和本地ID 没有实例ID,或者要卸载的对象的实例ID 引用无效文件GUID 和本地ID,则保留引用但不会加载实际的对象,比如我们在开发中运行应用程序时,或在“场景视图(Scene View)” 中,“(Missing)” 对象将以不同的方式显示,具体取决于其类型。例如,网格看起来是不可见的,而纹理可能看起来是洋红色,丢失了,就是这个原因造成的。

下面给读者介绍关于卸载对象的原理:
一:对象在发生未使用的资源清理时自动卸载,当场景被破坏或者切换时(即 SceneManager.LoadScene 时),或者当脚本调用Resources.UnloadUnusedAssets API 时,此过程仅卸载未引用的对象; 只有当没有 Mono 变量持有对对象的引用,并且没有其他对象持有对对象的引用时,才会卸载对象。此外,请注意,不会卸载任何标有HideFlags.DontUnloadUnusedAsset和HideFlags.HideAndDontSave的对象。
二:可以通过调用Resources.UnloadAsset API 卸载在Resources文件夹的对象,这些对象的实例ID 仍然有效,并且仍将包含有效的文件GUID 和本地ID,如果任何 Mono 变量或其他 Object 包含对使用Resources.UnloadAsset卸载的 Object 的引用,则只要取消引用,就会重新加载该 Object。
三:在调用AssetBundle.Unload(true)API 时,会立即自动卸载源 AssetBundles 的对象,这使得对象实例ID 的文件GUID 和本地ID 无效,并且对已卸载对象的任何实时引用将变为“(Missing)”引用。从C#脚本尝试访问卸载对象上的方法或属性时将产生NullReferenceException。这个在编程过程中也会遇到。
四:如果调用 AssetBundle.Unload(false),使得卸载的 AssetBundle 对象将不会被销毁,但 Unity 将使实例ID的文件GUID 和本地ID 引用无效,如果从内存中卸载这些对象并且仍然存在对已卸载对象的实时引用,则 Unity 将无法重新加载这些对象。

游戏对象层次结构

序列化 Unity 游戏对象的层次结构时,比如做预制体,层次结构中的每个 GameObject 和 Component 将在序列化数据中单独表示,这对加载和实例化 GameObjects 层次结构所需的时间产生了一定的影响。
在创建任何 GameObject 层次结构时,CPU 时间以几种不同的方式使用:
一:读取源数据(来自存储,来自 AssetBundle,或来自另一个 GameObject 等)
二:实例化新的 GameObjects 和组件
三:在主线程上唤醒新的 GameObjects 和 Components
读取源数据的时间随序列化到层次结构中的组件和游戏对象的数量线性增加,并且还要乘以数据源的速度。我们加载资源时,从内存中的其他位置读取数据要比从存储设备加载数据快得多,因此,当在存储速度较慢的平台上加载预制体时,从存储中读取预制体的序列化数据所花费的时间可能很快超过实例化预制体所花费的时间。也就是说,加载操作的成本与存储I / O 时间有关。

在序列化单个预制体时,每个 GameObject 和组件的数据都是单独序列化的,这可能会重复复制数据,例如,具有 30 个相同组件的 UI 将具有序列化30次的相同对象,从而产生大量二进制数据。在加载时,必须从磁盘读取这30个重复元素的每一个游戏对象(gameobject) 和组件(compontent) 的数据,然后再传输到新实例化的对象。此文件读取时间是实例化大型预制体的总体成本的重要因素,比较大的结构应该在模块化块中实例化,然后在运行时拼接在一起,这是一种理想情况。

总结

当实例化新的 GameObject(游戏对象) 时,使用 GameObject.Instantiate 接受父参数的重载变量,使用此重载可避免为新 GameObject 分配根变换层次结构,效率提升很多。


转载:https://blog.csdn.net/jxw167/article/details/102469897
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场