Unity 资产包的技巧和陷阱

资产包是包含游戏资产的归档文件。它们用于将游戏分割成逻辑块,让您可以按需提供和更新内容,同时缩小游戏构建的规模。它们还常用来为游戏提供补丁和 DLC。资产包可以包含预制件、材质、纹理、音频片段、场景等各种资产,但不能包含脚本。
以前,需要手动构建资产包,对每个资产进行相应的标记,然后在运行时自行跟踪和解决依赖关系。如今,所有这些都由 Addressables 系统负责,它会根据你定义的资产组为你构建资产包,并透明地加载和处理依赖关系。
虽然有很多指南介绍了资产包的工作原理,但我还是想介绍一下系统中一些鲜为人知的方面,重点是游戏性能、内存运行时使用情况和一般兼容性。
每当尝试使用捆绑包中包含的资产时,Unity 都会确保将相应的捆绑包加载到内存中,然后再将资产加载到内存中。
虽然可以在资产捆绑包中部分加载特定资产,但不允许反向加载。这意味着,一旦资产捆绑包内的某项资产被加载,只有在不再需要整组资产时才能卸载。
因此,如果您的捆绑结构不理想,随着游戏的进行,运行时内存使用量往往会不断增加,从而导致性能下降和潜在的崩溃。因此,最好避免捆绑大量资产,因为这样会占用大量运行内存,成为游戏的瓶颈。相反,应根据资产的装载和使用频率来打包资产。
资产捆绑包通常是向前兼容的,因此使用旧版本 Unity 构建的捆绑包在大多数情况下都可以在使用新版本 Unity 构建的游戏上运行(假设您没有剥离 TypeTree 信息,如后面所述)。反之亦然,因此基于比游戏构建所用版本更新的 Unity 版本构建的捆绑包不太可能正确加载。
随着捆绑包和用于游戏构建的引擎之间的版本差异增大,兼容的可能性就会降低。还有一种情况是,捆绑包可能仍然可以加载,但捆绑包中包含的对象却无法在新版 Unity 中正确加载,这很可能是由于对象的序列化方式发生了变化,从而产生了问题。在这种情况下,您需要重建捆绑包以保持兼容性。
从不同版本的 Unity 中加载捆绑包也会产生性能成本,这将在下文的 TypeTree 部分中介绍。
出于这些原因,建议您在更新游戏构建的 Unity 版本时,针对现有的资产包进行全面测试,同时尽可能更新它们。
资产包一般不提供跨平台支持。在编辑器中,您可以从另一个目标平台加载捆绑包,但在设备上加载会失败。
对于包含不一定特定于平台的资产的捆绑包,情况依然如此。
造成这种限制的原因是,数据的优化或压缩方式可能只适用于目标平台。此外,捆绑包可能包含特定于平台的数据,这些数据不应在不同平台之间共享,因此这可以防止泄漏不适合其他平台的内容。
加载缓存是一个共享页面池,Unity 会在其中存储最近访问过的资产包数据。这是全局性的,因此游戏中的所有资产包都可以共享。
这是最近才引入的,我相信是在 Unity 2021.3 上,然后回溯到了 2019.4。在此之前,Unity 依赖于为每个资产包提供单独的缓存,这导致运行时内存使用率大大增加(在下文 "序列化文件缓存 "中介绍)。
默认设置为 1MB,但可以通过设置AssetBundle.memoryBudgetKB 进行更改。
在大多数情况下,默认的缓存大小就足够了,不过在某些情况下,更改缓存大小可能会给游戏带来好处。例如,如果您的捆绑包中包含大量小对象,增加缓存大小可能会导致更多的缓存命中,从而提高游戏性能。
除了游戏资产外,资产包还包括大量额外的信息和标头,Unity 使用这些信息和标头来了解要加载哪些资产以及如何加载,同时还包括一个专用缓存(取决于您使用的 Unity 版本)。
捆绑包中资产的地图。通过它,您可以按名称查找和加载捆绑包中的每个资产。通常情况下,它在内存中的大小并不重要,除非你有包含数千个对象的特别大的资产包。
预加载表列出了捆绑包中每个资产的依赖关系。Unity 使用它来正确加载和构建资产。
如果您的捆绑包中包含的资产有很多显式和隐式依赖关系,以及来自其他捆绑包的级联依赖关系,那么这个数字可能会变得很大。出于这个原因(还有许多其他原因),设计捆绑包时最好尽量减少依赖链。
类型树定义了资产包所含对象的序列化布局。
它们的大小取决于捆绑包中包含多少种不同类型的对象。因此,最好避免将许多不同类型的对象混合在一起的大型捆绑包。
当您升级游戏构建的 Unity 版本,同时仍尝试加载基于旧版本引擎构建的资产包时,为了保持兼容性,TypeTrees 是必不可少的。例如,如果对象的格式或结构发生了变化,它们就会允许你进行安全二进制读取,这样 Unity 就可以尝试加载它,而不管它发生了什么变化。这需要付出性能代价,因此一般建议在更新引擎时尽可能更新捆绑包。
通过设置 BuildAssetBundleOptions.DisableWriteTypeTree标记来禁用。这将使捆绑包和相关内存开销更小,但也意味着每当更新游戏构建的引擎版本时,都需要重建所有捆绑包。如果用户生成的内容依赖于由播放器构建的捆绑包,这一点尤其令人头疼,因此除非有非常充分的理由,否则建议保持启用 TypeTrees。
通常可以安全禁用 TypeTrees 的一种情况是直接包含在游戏构建中的捆绑包。在这种情况下,升级引擎无论如何都需要制作新的游戏构建和新的资产包,因此其可追溯性方面并不重要。
每个捆绑包都有自己的 TypeTrees,因此多个包含相同类型对象的小捆绑包会略微增加磁盘的总大小。另一方面,TypeTrees 在加载时会存储在内存的全局缓存中,因此如果多个资产包存储相同类型的对象,也不会产生较高的运行时内存成本。
请注意:自 Unity 2019.4 版起,该缓存已被上文所述的全局共享 Loading 缓存所取代。
加载资产包时,Unity 会分配内部缓冲区,将其序列化文件存储到内存中。
常规资产包包含一个序列化文件,而流场景资产包中的每个场景最多包含两个文件。这些缓冲区的大小取决于平台。在 Switch、PlayStation 和 Windows RT 上将是 128KB,而其他平台的缓冲区都是 14KB。
因此,最好避免使用大量非常小的资产捆绑包,因为与实际提供的资产相比,这些缓冲区占用的内存可能会非常大。
CRC(循环冗余校验)用于对资产包进行校验和验证,确保交付给游戏的内容与您期望的完全一致。CRC 是根据未压缩的数据包内容计算的。
在游戏机上,"资产包 "通常作为标题安装的一部分包含在 Localization 存储器中,或作为 DLC 下载,因此无需进行 CRC 检查。在其他平台上,如 PC 或移动平台,对从 CDN 下载的捆绑包进行 CRC 检查非常重要。这是为了确保文件不会损坏或截断,从而导致潜在的崩溃,同时也是为了避免潜在的篡改。
就 CPU 占用率而言,CRC 检查相当昂贵,尤其是在游戏机和移动设备上。由于这些原因,在 Localization 和缓存捆绑包上禁用 CRC 检查,只在非缓存远程捆绑包上启用 CRC 检查,通常是一个很好的折衷办法。
默认情况下,Unity 提供了三种查找捆绑包内资产的方法:
- 项目相对路径(Assets/Prefabs/Characters/Hero.prefab)
- 资产文件名(英雄)
- 带扩展名的资产文件名 (Hero.prefab)
虽然这很方便,但也要付出代价。为了支持后两种方法,Unity 需要建立查找表,这对于大型捆绑包来说会消耗大量内存。
此外,使用与 "项目相对路径 "不同的方法加载资产会产生性能成本,这也是因为需要查表。
因此,建议避免使用这些方法。您甚至可以在创建资产包时禁用它们,这将提高资产包的加载性能和运行时内存使用率。
为此,您可以在构建捆绑包时设置这两个标志:
- BuildAssetBundleOptions.DisableLoadAssetByFileName
- BuildAssetBundleOptions.DisableLoadAssetByFileNameWithExtension
要了解有关资产管理的更多信息,分享反馈意见,或与社区和 Unity 工作人员交流,请查看资产管理论坛。