任何高质量游戏都有一个关键要素——有效的资产输入和输出内存,多年来Unity一直在努力提高用户项目的性能。今天我们将来分享一些关于如何利用Unity地址追踪资产系统来增强用户的内容加载策略。
内存是一种稀缺资源,因此必须小心的进行管理,尤其是在将项目移植到新平台时。使用地址追踪系统可以通过引入弱引用来防止加载不必要的资产,从而提高运行时内存。弱引用意味着用户可以控制被引用的资产何时加载到内存中以及何时从内存中取出;地址追踪系统会找到所有必要的依赖项,并加载它们。
本篇文章将涵盖在使用Unity地址追踪资产系统设置项目时可能遇到的方案和问题,并解释如何识别它们并及时修复它们。
库存示例
我们将使用一个简单的示例来进行展示。
在场景中我们建立了一个库存管理员脚本,它引用了Unity的三个库存资产:剑、Boss剑、盾牌预置。
这些资产在游戏中并不总是需要的。
我们可以使用预览包内存分析器在游戏运行时查看内存。在Unity 2020 LTS中,则必须首先在项目设置中启用预览包,然后才能从包管理器安装此包。
而如果你使用的是Unity 2021.1,请在「包管理器」窗口中的附加菜单(+)中选择按名称添加包的选项。
第一阶段 : 硬引用, 无需寻址
让我们从最基本的实现开始,然后朝着设置地址追踪内容的最佳方法前进。我们将简单地应用硬引用(由检查器的直接分配,被GUID追踪)到我们的预设中。
加载场景时,场景中的所有对象及其依赖项也会加载到内存中。这意味着在我们的清单系统中列出的每一个预置项,以及这些预置项的所有依赖项(纹理、网格、音频等)都将存在在内存中。)
当我们开始构建并使用内存分析器拍摄快照时,我们可以看到资产的纹理已经存储在内存中,尽管它们还没有被实例化。
项目的纹理存储在内存中,即使它们还没有被实例化。
存在问题:内存中有我们目前不需要的资产。在有大量库存条目的项目中,在运行时这将导致相当大的内存压力。
第二 阶段 : 实现 地址追踪
为了避免加载不需要的资产,我们将改变了库存系统,以使用地址追踪系统。使用资源引用而不是直接引用可以防止这些对象随场景一起加载。我们将库存预置移动到一个地址追踪组,并使用地址追踪应用编程接口(API)改变库存系统来实例化和释放对象。
构建播放器并拍摄快照。请注意,内存中没有任何资产,因为它们还没有被实例化。
内存中没有库存物品纹理;仅有TextMeshPro纹理。
实例化所有项目,以查看它们在内存中的资产是否正确显示。
存在问题:如果我们实例化我们所有的物品并销毁boss剑,我们仍然会在内存中看到boss剑的纹理「BossSword_E」,即使它没有被使用。原因是,虽然可以部分加载资产包,但不可能自动部分卸载它们。这种行为对于包含许多资产的包来说尤其成问题,比如包含我们所有库存预置的单个资产包。在不再需要整个资产捆绑包之前,或者在我们调用昂贵的CPU操作资源之前,捆绑包中的任何资产都不会卸载。
BossSword_E的纹理即使在boss剑被删除后依然保留在记忆中。
第三阶段: 更小的捆绑包
要解决这个问题,我们必须改变我们组织资产的方式。虽然我们目前有一个单独的地址追踪组,它将其所有资产打包成一个资产包,但是我们可以为每个预置创建一个资产包。这些更小的资产捆绑包减轻了大捆绑包在内存中保留我们不再需要的资产的问题。
做出这种改变很容易。选择一个地址追踪组,然后选择内容打包和加载——高级选项——捆绑模式,并转到检查器将捆绑模式从打包中拆分出来。
通过使用分开包装要构建这个地址追踪组,您可以为地址追踪组中的每个资产创建一个资产文件夹。
资产和捆绑包如下所示:
现在,回到我们最初的测试:找到我们的三个项目,然后去掉boss剑的纹理。我们会发现boss剑的纹理现在被卸载了,因为我们现在把它拆分开来了。
存在问题:如果我们生成所有三个项目并获取内存捕获,重复的资产将出现在内存中。更具体地说,这将导致纹理「剑_N「」和「剑_D「」产生多个副本。如果我们只改变软件包的数量,这又能否实现呢?
第四阶段: 修复重复的资产
为了回答这个问题,让我们考虑一下我们创建的三个包中的所有内容。虽然我们只将三个预置资产放入捆绑包中,但是有额外的资产作为预置的依赖项被拉进那些捆绑包中。例如,剑预制资产也有网格、材料和纹理资产等。如果这些依赖项没有明确地包含在地址表的其他地方,那么它们会被自动添加到每个需要它们的软件包中。
我们的剑和Boss剑的资产包包含一些相同的依赖关系。
地址追踪功能包括一个分析窗口,以帮助诊断软件包布局。打开窗口——资产管理——地址追踪——分析运行规则软件包布局预览。在这里,我们看到剑的包中明确地包括了剑. prefib,但是有许多隐含的依赖关系也被拉入到这个软件包中。
在同一窗口中,运行检查重复的捆绑依赖项。该规则基于我们当前的地址追踪布局突出显示了多个资产包中包含的资产。
分析表明,剑的软件包之间有重复的纹理和网格,所有三个包都复制了相同的着色器。
我们可以通过两种方式防止这些资产重复使用:
1、将剑、boss剑和盾牌预置放在同一个包中,以便它们共享依赖关系
2、在地址追踪的某个地方明确标明包含重复的资产
我们希望避免将多个库存预置放在同一个包中,使不需要的资产从内存中消失。因此,我们将把重复的资产添加到它们自己的捆绑包(捆绑包4和捆绑包5)中。
重复的纹理被明确地放置在它们自己的包中。
除了分析我们的包之外,分析规则还可以通过以下方式自动修复违规资产修复选定的规则。按此按钮创建一个名为「重复资产隔离」的新地址追踪组,其中包含四个重复资产。将该组的捆绑模式设置为分开包装以防止不再需要的任何其他资产保留在内存中。
第五 阶 端: 在大型项目中减少资产捆绑元数据的大小
使用这种资产捆绑策略可能会导致大规模的问题。对于给定时间加载的每个资产元数据,都有内存开销。如果我们将当前策略扩展到数百或数千个库存项目,这些元数据可能会消耗难以计数的内存量。
在Unity的分析器中查看当前资产元数据的内存开销。转到内存模块并拍摄内存快照。在类别中查找其他——序列化文件。
目前有1,819个包加载了序列化文件内存,总大小为263 MB。
对于每个加载的资产文件,内存中都有一个序列化的文件条目。这些内存是资产包元数据,而不是软件包中的实际资产。这些元数据包括:
l 两个文件读取缓冲区
l 列出包中包含的每个唯一类型的类型树
l 指向资产的目录
在这三项元数据中,文件读取缓冲区占用的空间最大。这些缓冲区在PS4、Switch和Windows RT上为64 KB,在所有其他平台上为7 KB。在上面的示例中,1,819个包* 64 KB * 2个缓冲区= 227 MB(仅用于缓冲区)。
鉴于缓冲区的数量与资产包的数量成线性比例,减少内存的简单解决方案是在运行时加载更少的包。然而,我们以前避免了加载大的包,以防止不需要的资产留存在内存中。那么,我们如何在保持粒度的同时减少包的数量呢?
第一步是根据资产在应用程序中的用途将它们组合在一起。如果你能根据你的应用程序做出明智的假设,那么你就可以将那些你知道总是会一起加载和卸载的资产分组,比如那些根据它们所处的游戏级别分组的资产。
另一方面,你可能处于这样一种情况,你不能对何时需要/不需要你的资产做出安全的假设。例如,如果你正在创建一个开放世界的游戏,那么你不能简单地把森林生物群落中的所有东西都组合成一个资产包,因为你的玩家可能会从森林中抓取一个项目,并在生物群落之间携带它。因为玩家仍然需要森林中的那一个资产,所以整个森林捆绑包会保留在内存中。
幸运的是,现在有一种方法可以减少包的数量,同时保持所需的粒度级别。让我们更明智地对待如何对包进行重复数据消除。
我们运行Unity内置的重复数据消除分析规则可检测多个捆绑包中的所有资产,并将它们高效地移动到单个地址追踪组中。通过将该组设置为分开包装,我们最终会得到一个资产包。然而,有一些重复的资产,我们可以安全地打包在一起,而不会导致引入内存的问题。参照下图:
我们知道纹理「剑_N」和「剑_D」在同一个包(软件包1和软件包2)中属于依赖关系。因为这些纹理有相同的用途和来源,所以我们可以安全地将它们打包在一起,而不会造成内存问题。两种剑的纹理总是同时加载或卸载。所以我们永远不会担心其中一个纹理可能会保留在内存中,因为永远不会有我们专门使用一个纹理而不使用另一个纹理的情况。
在这个简单的项目中,我们只是将包的总数从七个减少到五个。但是想象一下这样一个场景,您的应用程序的地址追踪资产中有数百、数千甚至更多的重复资产。如:Unity与Unknown Worlds Entertainment公司合作,为他们的游戏Subnautica提供专业服务,在使用内置的重复数据消除分析规则后,该项目最初共有8,718个包。在应用自定义规则,根据已消除重复数据的资产包的父项对其进行分组后,我们将其减少到5,199个包。
这意味着包的数量减少了40%,同时包中仍然有相同的内容,并保持相同的粒度级别。软件包数量减少了40%,运行时序列化文件的大小也减少了40%(从311MB到184MB)。
结论
使用地址追踪功能可以显著降低内存消耗。通过组织资产包以适应用例,你可以进一步减少内存的使用量。毕竟,为了适合所有应用程序,内置的分析规则是保守的。通过编写你自己的分析规则还可以实现自动化软件包布局而且你也可以继续优化它。为了找到更多内存的问题,你需要继续经常进行概要分析,并检查分析窗口,以查看哪些重复资产被显式和隐式地包含在软件包之中。