本系列文章翻译自:
https://learn.unity.com/tutorial/assets-resources-and-assetbundles

这是一系列文章,深入讨论了Unity引擎中的资产(Asset)和资源管理(Resource Management)。旨在为高级开发人员提供Unity资产和序列化系统的深入原理的知识。研究了Unity的AssetBundle系统的技术基础和当前使用它们的最佳使用方法。

该指南分为四章:

  1. 资产,对象和序列化(Assets,Objects and serialization)讨论了Unity如何序列化资产和处理资产之间引用的底层细节。强烈建议读者从本章开始,因为它介绍了整个指南中使用的术语。
  2. Resources 文件夹 讨论了内置的Resources API。
  3. AssetBundle 基础知识 是建立在第1章的知识基础上,用于描述 AssetBundles 的运作方式,并讨论 AssetBundles 的加载和 AssetBundles 的资产加载。
  4. AssetBundle 使用指南 是一篇长篇文章,讨论 AssetBundles 实际使用的许多方式。包括将有关将资产分配给 AssetBundles 和管理已加载资产的部分,并描述了使用 AssetBundles 的开发人员遇到的许多常见问题。

注意:本指南的 ObjectsAssets 术语与Unity的API中命名有所不同。

本指南调用 Objects 的数据在许多Unity API 中称为Assets,例如AssetBundle.LoadAssetResources.UnloadUnusedAssets。本指南称之为 Assets 的文件很少暴露给任何公共API。当它们被公开时,通常只在与构建相关的代码中,例如AssetDatabaseBuildPipeline。在这些情况下,它们在公共API 中称为文件(files)

0×00 前言

本系列的上一步介绍了 AssetBundles基础知识,其中包括各种加载API的低级行为。本章讨论在实践中使用 AssetBundle 的各个方面的问题和可能的解决方案。

0×01 管理已加载的资产

在内存敏感的环境中仔细控制加载的对象的大小和数量至关重要。从活动场景中删除对象时,Unity 不会自动卸载对象。资产清理在特定时间触发,也可以手动触发。

必须谨慎管理 AssetBundles 本身。由本地存储上的文件(在Unity缓存中或通过 AssetBundle.LoadFromFile 加载的文件)支持的 AssetBundle 具有最小的内存开销,很少消耗超过几十千字节。但是,如果存在大量 AssetBundle,这种开销仍然会成为问题。

由于大多数项目允许用户重新体验内容(例如重放),因此了解何时加载或卸载 AssetBundle 非常重要。如果 AssetBundle 未正确卸载,则可能导致内存中的对象重复。不正确地卸载 AssetBundles 在某些情况下也会导致不良行为,例如导致纹理丢失。要了解为什么会发生这种情况,请参阅“ 资产”,“对象”和“序列化” 步骤的“ 对象间引用” 部分。

管理资产和 AssetBundles 时最重要的一点是,在调用 AssetBundle.Unload 时,对于 unloadAllLoadedObjects 参数,其值为 true 或 false。

此 API 将卸载正在调用的 AssetBundle 的标头信息。所述 unloadAllLoadedObjects 参数确定是否还需卸载这个 AssetBundle 实例化的所有对象。如果设置为 true ,那么源自 AssetBundle 的所有对象也将立即卸载,即使它们当前正在活动场景中使用。

例如,假设材料 M 是从 AssetBundle AB 加载的,并假设 M 当前处于活动场景中。

如果调用了 AssetBundle.Unload(true),则 M 将从场景中删除,销毁并卸载。但是,如果调用了 AssetBundle.Unload(false),那么 AB 的头信息将被卸载,但是 M 将保留在场景中并且仍然可以正常工作。调用 AssetBundle.Unload(false)会中断 MAB 之间的链接。如果稍后再次加载 AB ,则 AB 中包含的对象的新副本将被加载到内存中。

如果稍后再次加载 AB ,则将重新加载 AssetBundle 标头信息的新副本。然而,中号是不是从这个新副本加载 AB 。Unity 不会在 ABM 的新副本之间建立任何链接。

如果调用 AssetBundle.LoadAsset() 来重新加载 M ,Unity 将不会将 M 的旧副本解释为 AB 中数据的实例。因此,Unity将加载 M 的新副本,并且场景中将有两个相同的 M 副本。

对于大多数项目,这种行为是不可取的。大多数项目应该使用 AssetBundle.Unload(true) 并采用一种方法来确保不重复对象。两种常用方法是:

  1. 在应用程序的生命周期中,在卸载 AssetBundle时具有明确定义的点,例如在级别之间或在加载屏幕期间。这是更简单和最常见的选择。
  2. 维护单个对象的引用计数,并仅在所有组成对象未使用时卸载 AssetBundle。这允许应用程序卸载和重新加载单个对象而不复制内存。

如果应用程序必须使用 AssetBundle.Unload(false),则只能以两种方式卸载单个对象:

  1. 消除场景和代码中对不需要的 Object 的所有引用。完成此操作后,调用 Resources.UnloadUnusedAssets
  2. 非增加性地加载场景。这将破坏当前场景中的所有对象并自动调用 Resources.UnloadUnusedAssets

如果项目具有明确定义的点,用户可以等待对象加载和卸载,例如在游戏模式或级别之间,则应使用这些点来根据需要卸载任意数量的对象并加载新对象。

最简单的方法是将项目的离散块打包到场景中,然后将这些场景及其所有依赖项构建到 AssetBundles 中。然后,应用程序可以进入“加载”场景,完全卸载包含旧场景的 AssetBundle,然后加载包含新场景的 AssetBundle。

虽然这是最简单的流程,但有些项目需要更复杂的 AssetBundle 管理。由于每个项目都不同,因此没有通用的 AssetBundle 设计模式。

在决定如何将对象分组为 AssetBundles 时,通常最好先将对象捆绑到 AssetBundle 中(如果必须同时加载或更新它们)。例如,考虑角色扮演游戏。可以按场景将各个地图和过场动画分组为 AssetBundle,但在大多数场景中都需要一些对象。可以构建 AssetBundles 以提供肖像,游戏内UI以及不同的角色模型和纹理。然后可以将这些后面的对象和资产分组到第二组 AssetBundle 中,这些 AssetBundle 在启动时加载并在应用程序的生命周期内保持加载状态。

如果 Unity 必须在卸载 AssetBundle 之后从其 AssetBundle 重新加载 Object,则会出现另一个问题。在这种情况下,重新加载将失败,并且对象将作为(缺失)对象出现在 Unity 编辑器的层次结构中。

这主要发生在 Unity 失去并重新获得对其图形上下文的控制时,例如当移动应用程序被挂起或用户锁定其PC时。在这种情况下,Unity 必须将纹理和着色器重新上传到 GPU。如果这些资源的源 AssetBundle 不可用,则应用程序将将场景中的对象呈现为洋红色。

0×02 分配

将项目的 AssetBundle 分发给客户端有两种基本方法:与项目同时安装或在安装后下载它们。

是否在安装期间或之后传输 AssetBundle 的决定是由项目运行的平台的功能和限制驱动的。移动项目通常选择安装后下载以减少初始安装大小,并保持低于无线下载大小限制。控制台和PC项目通常会在初始安装时传输 AssetBundles。

无论最初如何交付 AssetBundle,正确的体系结构都允许在安装后将新的或修订的内容修补到项目中。有关此内容的更多信息,请参阅Unity手册中的 “使用AssetBundles修补“ 部分。

1. 与项目同时安装

使用该种方式传输 AssetBundles 是分发它们的最简单方法,因为它不需要额外的下载管理代码。项目可能包含 AssetBundles 和安装有两个主要原因:

  • 减少项目构建时间并允许更简单的迭代开发。如果这些 AssetBundle 不需要与应用程序本身分开更新,那么 AssetBundle 可以通过将 AssetBundle 存储在 Streaming Assets 中而包含在应用程序中。请参阅下面的Streaming Assets 部分。
  • 发布可更新内容的初始修订版。这通常是为了在最初安装后节省最终用户的时间,或者作为以后修补的基础。流式资产对于这种情况并不理想。但是,如果不能编写自定义下载和缓存系统,则可以从 Streaming Assets 将可更新内容的初始修订加载到 Unity 缓存中(请参阅下面的缓存启动部分)。

1) 流媒体资产 (Streaming Assets)

在安装时在 Unity 应用程序中包含任何类型的内容(包括AssetBundle)的最简单方法是在构建项目之前将内容构建到 / Assets / StreamingAssets / 文件夹中。构建时 StreamingAssets 文件夹中包含的任何内容都将复制到最终应用程序中。

可以在运行时通过属性 Application.streamingAssetsPath 访问本地存储上 StreamingAssets 文件夹的完整路径。然后可以在大多数平台上通过 AssetBundle.LoadFromFile 加载 AssetBundle

Android 开发者:在 Android上,StreamingAssets 文件夹中的资源存储在 APK 中,如果压缩它们可能需要更多时间加载,因为存储在 APK 中的文件可以使用不同的存储算法。所使用的算法可能因 Unity 版本而异。您可以使用 7-zip 等存档打开 APK 以确定文件是否已压缩。如果是,您可以期望 AssetBundle.LoadFromFile() 执行得更慢。在这种情况下,您可以使用 UnityWebRequest.GetAssetBundle 检索缓存版本作为一种解决方法。通过使用 UnityWebRequest,AssetBundle 将在第一次运行期间解压缩并缓存,从而允许后续执行更快。请注意,这将占用更多存储空间,因为 AssetBundle 将被复制到缓存中。或者,您可以导出 Gradle 项目并在构建时向 AssetBundles 添加扩展。然后,您可以编辑 build.gradle 文件并将该扩展添加到 noCompress 部分。完成后,您应该能够使用 AssetBundle.LoadFromFile() 而无需支付减压性能成本。

注意:Streaming Assets 在某些平台上不是可写位置。如果项目的 AssetBundles 需要在安装后更新,请使用 WWW.LoadFromCacheOrDownload 或编写自定义下载程序。

2. 下载后安装

向移动设备提供 AssetBundles 的首选方法是在安装应用程序后下载它们。这还允许在安装后更新内容,而不强制用户重新下载整个应用程序。在许多平台上,应用程序二进制文件必须经过昂贵且冗长的重新认证过程。因此,开发一个良好的安装后下载系统至关重要。

提供 AssetBundles 的最简单方法是将它们放在 Web 服务器上并通过 UnityWebRequest 提供。Unity 将自动将下载的 AssetBundle 缓存在本地存储上。如果下载的 AssetBundle 是 LZMA 压缩的,则 AssetBundle 将作为未压缩或重新压缩存储在缓存中,如LZ4(取决于 Caching.compressionEnabled 设置),以便将来加载更快。如果下载的包是 LZ4 压缩的,则 AssetBundle 将被压缩存储。如果缓存填满,Unity 将从缓存中删除最近最少使用的 AssetBundle。有关更多详细信息,请参阅内置缓存部分。

通常建议使用来启动 UnityWebRequest 如果可能,或 WWW.LoadFromCacheOrDownload 只有使用 Unity 5.2 或以上。如果内置 API 的内存消耗,缓存行为或性能对于特定项目是不可接受的,或者项目必须运行特定于平台的代码以实现其要求,则仅投资自定义下载系统。

可能阻止使用 UnityWebRequestWWW.LoadFromCacheOrDownload 的情况示例:

  • 当需要对 AssetBundle 缓存进行精细控制时;
  • 当项目需要实现自定义压缩策略时;
  • 当项目希望使用特定于平台的 API 来满足某些要求时,例如在非活动时需要流式传输数据。
  • 示例:使用 iOS 的后台任务 API 在后台下载数据。
  • 必须在 Unity 没有正确 SSL 支持的平台(例如PC)上通过 SSL 提供 AssetBundle。

3. 内置缓存

Unity 有一个内置的 AssetBundle 缓存系统,可以用来缓存通过 UnityWebRequest API 下载的 AssetBundles ,它有一个重载接受一个 AssetBundle 版本号作为参数。此数字不存储在 AssetBundle 内,并且不由 AssetBundle 系统生成。

缓存系统跟踪传递给 UnityWebRequest 的最新版本号。当使用版本号调用此 API 时,缓存系统会通过比较版本号来检查是否存在缓存的 AssetBundle。如果这些数字匹配,系统将加载缓存的 AssetBundle。如果数字不匹配,或者没有缓存的 AssetBundle,则 Unity 将下载新副本。此新副本将与新版本号相关联。

缓存系统中的 AssetBundle 仅由其文件名标识,而不是由下载它们的完整 URL 标识。这意味着具有相同文件名的 AssetBundle 可以存储在多个不同的位置,例如内容分发网络。只要文件名相同,缓存系统就会将它们识别为相同的 AssetBundle。

每个应用程序都可以确定将版本号分配给 AssetBundles 的适当策略,并将这些数字传递给 UnityWebRequest 。这些数字可能来自各种类型的唯一标识符,例如 CRC 值。请注意,虽然 AssetBundleManifest.GetAssetBundleHash() 也可用于此目的,但我们不建议使用此函数进行版本控制,因为它只提供估算,而不是真正的哈希计算。

有关详细信息,请参阅Unity手册中的 “使用AssetBundles修补” 部分。

在Unity 2017.1之后,通过允许开发人员从多个缓存中选择活动缓存,提供了 Caching API 以提供更精细的控制。Unity的早期版本可能只修改 Caching.expirationDelayCaching.maximumAvailableDiskSpace 以删除缓存的项目(这些属性保留在Unity 2017.1的 Cache类 中)。

expirationDelay 是自动删除 AssetBundle 之前必须经过的最小秒数。如果在此期间未访问 AssetBundle,则会自动删除它。

maximumAvailableDiskSpace 指定缓存在开始删除比 expirationDelay 最近使用的 AssetBundle 之前可能使用的本地存储空间量(以字节为单位)。达到限制时,Unity 将删除最近最少打开的缓存中的 AssetBundle(或标记为通过 Caching.MarkAsUsed 使用)。Unity 将删除缓存的 AssetBundle,直到有足够的空间来完成新的下载。

1) 启动缓存

由于 AssetBundles 由其文件名标识,因此可以使用应用程序附带的 AssetBundle “填充”缓存。为此,请将每个 AssetBundle 的初始版本或基本版本存储在 / Assets / StreamingAssets / 中。该过程与 Shipped with project 部分中详述的过程相同。

可以通过在第一次运行应用程序时从 Application.streamingAssetsPath 加载 AssetBundles 来填充缓存。从那时起,应用程序可以正常调用 UnityWebRequest (UnityWebRequest 也可以用于从 StreamingAssets 路径初始加载 AssetBundle)。

4. 自定义下载程序

编写自定义下载程序使应用程序可以完全控制 AssetBundles 的下载,解压缩和存储方式。由于涉及的工程工作非常重要,我们建议这种方法仅适用于大型团队。编写自定义下载程序时有四个主要注意事项:

  • 下载机制
  • 存储位置
  • 压缩类型
  • 修补

有关修补 AssetBundles 的信息,请参阅 Unity 手册中的 Patching with AssetBundles 部分。

1) 下载

对于大多数应用程序,HTTP 是下载 AssetBundles 的最简单方法。但是,实现基于 HTTP 的下载器并不是最简单的任务。自定义下载程序必须避免过多的内存分配,过多的线程使用和过多的线程唤醒。Unity 的 WWW 类不适用于 AssetBundle基础 步骤的 WWW.LoadFromCacheOrDownload 部分中详尽描述的原因。

编写自定义下载程序时,有三个选项:

  • C#的 HttpWebRequest 和 WebClient 类;
  • 自定义本机插件;
  • 资产商店包;
a. C#类

如果应用程序不需要 HTTPS / SSL 支持,C#的 WebClient 类提供了最简单的下载 AssetBundle 的机制。它能够将任何文件异步下载到本地存储,而无需过多的托管内存分配。

要使用 WebClient 下载AssetBundle,请分配该类的实例,并将要下载的 AssetBundle 的 URL 和目标路径传递给它。如果需要对请求的参数进行更多控制,则可以使用C#的 HttpWebRequest 类编写下载程序:

  1. HttpWebResponse.GetResponseStream 获取字节流。
  2. 在堆栈上分配固定大小的字节缓冲区。
  3. 从响应流读入缓冲区。
  4. 使用 C# 的 File.IO API 或任何其他流 IO 系统将缓冲区写入磁盘。
b. 资产商店包

多个资产商店包提供本机代码实现,以通过 HTTP,HTTPS 和其他协议下载文件。在为 Unity 编写自定义本机代码插件之前,建议您评估可用的 Asset Store 软件包。

c. 自定义本机插件

编写自定义本机插件是最耗时但最灵活的在 Unity 中下载数据的方法。由于编程时间要求高,技术风险高,只有在其他方法无法满足应用要求的情况下,才推荐使用此方法。例如,如果应用程序必须在 Unity 中没有 C#SSL 支持的平台上使用 SSL 通信,则可能需要自定义本机插件。

自定义本机插件通常会包装目标平台的本机下载API。示例包括iOS 上的NSURLConnection和Android 上的java.net.HttpURLConnection。有关使用这些API的更多详细信息,请参阅每个平台的本机文档。

2) 存储

在所有平台上,Application.persistentDataPath 都指向一个可写位置,该位置应该用于存储应该在应用程序运行之间保留的数据。编写自定义下载程序时,强烈建议使用 Application.persistentDataPath 的子目录来存储下载的数据。

Application.streamingAssetPath 不可写,对于 AssetBundle 缓存来说是一个糟糕的选择。streamingAssetsPath 的示例位置包括:

  • OSX :在 .app 包中; 不可写
  • Windows :在安装目录中(例如 Program Files ); 通常不可写
  • iOS :在 .ipa 包内; 不可写
  • Android :在 .apk 文件中; 不可写

0×03 资产分配策略

决定如何将项目的资产划分为 AssetBundle 并不简单。采用简单的策略是很诱人的,例如将所有对象放在他们自己的 AssetBundle 中或只使用一个 AssetBundle,但这些解决方案有明显的缺点:

  • 资产捆绑太少......
  • 增加运行时内存使用量
  • 增加加载时间
  • 需要更大的下载量
  • 拥有太多 AssetBundles ......
  • 增加构建时间
  • 可以使开发复杂化
  • 增加总下载时间

关键决定是如何将对象分组为 AssetBundles。主要策略是:

  • 逻辑实体
  • 对象类型
  • 并发内容

有关这些分组策略的更多信息,请参见手册

0×04 常见的陷阱

本节介绍使用 AssetBundle 的项目中常见的几个问题。

1. 资产重复

当 Object 被构建到 AssetBundle 中时,Unity 5 的 AssetBundle 系统将发现 Object 的所有依赖关系。此依赖关系信息用于确定将包含在 AssetBundle 中的对象集。

显式分配给 AssetBundle 的对象将仅构建到该 AssetBundle 中。当 Object 的 AssetImporter 将其 assetBundleName 属性设置为非空字符串时,“显式"指定 Object 。这可以在 Unity Editor 中通过在 Object's Inspector 中选择 AssetBundle 或从 Editor 脚本中完成。

通过将对象定义为 AssetBundleBuild 映射的一部分,也可以将对象分配给 AssetBundle ,该映射将与重载的 BuildPipeline.BuildAssetBundles() 函数一起使用,该函数接收 AssetBundleBuild 数组。

未在 AssetBundle 中显式分配的任何 Object 将包含在包含一个或多个引用未标记对象的对象的所有 AssetBundle 中。

例如,如果将两个不同的对象分配给两个不同的 AssetBundle,但两者都引用了公共依赖关系对象,则该依赖关系对象将被复制到两个 AssetBundles 中。重复的依赖项也将被实例化,这意味着依赖项 Object 的两个副本将被视为具有不同标识符的不同对象。这将增加应用程序的 AssetBundles 的总大小。如果应用程序加载其父项,则还会导致将两个不同的 Object 副本加载到内存中。

有几种方法可以解决这个问题:

  1. 确保内置于不同 AssetBundle 的对象不共享依赖项。任何共享依赖项的对象都可以放在同一个 AssetBundle 中,而不会复制它们的依赖项。
    • 对于具有许多共享依赖项的项目,此方法通常不可行。它生成单片 AssetBundle,必须经常重建和重新下载才能方便或高效。
  2. 对 AssetBundles 进行分段,以便不会同时加载共享依赖关系的两个 AssetBundle。
    • 此方法可能适用于某些类型的项目,例如基于级别的游戏。但是,它仍然不必要地增加了项目 AssetBundles 的大小,并增加了构建时间和加载时间。
  3. 确保所有依赖项资产都内置在自己的 AssetBundle 中。这完全消除了重复资产的风险,但也带来了复杂性。应用程序必须跟踪 AssetBundle 之间的依赖关系,并确保在调用任何 AssetBundle.LoadAsset API 之前加载了正确的 AssetBundle 。

通过位于 UnityEditor 命名空间中的 AssetDatabase API 跟踪对象依赖。正如命名空间所暗示的,此API仅在 Unity 编辑器中可用,而不是在运行时。AssetDatabase.GetDependencies 可用于查找特定对象或资产的所有直接依赖项。请注意,这些依赖项可能有自己的依赖项。此外,AssetImporter API 可用于查询分配了任何特定 Object 的 AssetBundle。

通过组合 AssetDatabaseAssetImporter API,可以编写一个 Editor 脚本,确保将所有 AssetBundle 的直接或间接依赖项分配给 AssetBundles,或者没有两个 AssetBundle 共享尚未分配给 AssetBundle 的依赖项。由于复制资产的内存成本,建议所有项目都有这样的脚本。

2. Sprite 图集重复

任何自动生成的 sprite 图集都将被分配给包含从中生成 sprite 图集的 sprite 对象的 AssetBundle。如果 sprite 对象被分配给多个 AssetBundle,则 sprite 图集将不会被分配给 AssetBundle 并且将被复制。如果 sprite 对象未分配给 AssetBundle,则 sprite图集 也不会分配给 AssetBundle。

为了确保 sprite 图集不重复,请检查标记到相同 sprite 图集 的所有 sprite 都被分配到同一个 AssetBundle。

请注意,在 Unity 5.2.2p3 及更早版本中,自动生成的 sprite 图集永远不会分配给 AssetBundle。因此,它们将被包含在任何包含其组成 sprite 的 AssetBundle 中,以及任何引用其组成 sprite 的 AssetBundles。由于这个问题,强烈建议使用 Unity 的 sprite packer 的所有 Unity 5 项目升级到 Unity 5.2.2p4,5.3或任何更新版本的 Unity。

3. Android 纹理

由于 Android 生态系统中的设备分类很多,因此通常需要将纹理压缩为多种不同的格式。虽然所有 Android 设备都支持 ETC1,但 ETC1 不支持带 alpha 通道的纹理。如果应用程序不需要 OpenGL ES 2 支持,解决问题的最简单方法是使用 ETC2,所有 Android OpenGL ES 3 设备都支持 ETC2。

大多数应用程序需要在 ETC2 支持不可用的旧设备上发布。解决此问题的一种方法是使用 Unity 5 的 AssetBundle Variants(有关其他选项的详细信息,请参阅 Unity 的 Android 优化指南)。

要使用 AssetBundle Variants,所有无法使用 ETC1 进行干净压缩的纹理必须隔离到仅纹理的 AssetBundle 中。接下来,使用特定于供应商的纹理压缩格式(如 DXT5,PVRTC 和 ATITC),创建这些 AssetBundle 的足够变体,以支持 Android 系统中不支持 ETC2 的切片。对于每个 AssetBundle Variant,将包含的纹理的 TextureImporter 设置更改为适合 Variant 的压缩格式。

在运行时,可以使用 SystemInfo.SupportsTextureFormat API 检测对不同纹理压缩格式的支持。此信息应用于选择和加载包含以受支持格式压缩的纹理的 AssetBundle Variant。

有关 Android 纹理压缩格式的更多信息,请访问此处

4. iOS 文件处理过度使用

当前版本的 Unity 不受此问题的影响。

在 Unity 5.3.2p2 之前的版本中,Unity 会在 AssetBundle 加载的整个时间内保存到 AssetBundle 的打开文件句柄。这在大多数平台上都不是问题。但是,iOS 限制进程可能同时打开 255 的文件句柄数。如果加载 AssetBundle 导致超出此限制,则加载调用将失败,并显示 “Too Many Open File Handles” 错误。

对于试图将内容划分为数百或数千个 AssetBundle 的项目而言,这是一个常见问题。

对于无法升级到修补版 Unity 的项目,临时解决方案是:

  • 通过合并相关的 AssetBundle 减少正在使用的 AssetBundle 数量;
  • 使用 AssetBundle.Unload(false) 关闭 AssetBundle 的文件句柄,并手动管理加载的对象的生命周期;

0×05 AssetBundle Variants

AssetBundle 系统的一个关键特性是引入 AssetBundle Variants。Variants 的目的是允许应用程序调整其内容以更好地适应其运行时环境。变量允许不同 AssetBundle 文件中的不同 UnityEngine.Objects 在加载对象和解析实例 ID 引用时显示为“相同”对象。从概念上讲,它允许两个 UnityEngine.Objects 共享相同的文件 GUID 和本地 ID,并通过字符串 Variant ID 标识要加载的实际 UnityEngine.Object。

该系统有两个主要用例:

  1. Variant 简化了适用于给定平台的 AssetBundle 的加载。
    • 示例:构建系统可能会创建一个 AssetBundle,其中包含适用于独立 DirectX11 Windows 构建的高分辨率纹理和复杂着色器,以及另一个具有适用于 Android 的低保真内容的 AssetBundle。在运行时,项目的资源加载代码可以为其平台加载适当的 AssetBundle Variant,而传递给 AssetBundle.Load API 的 Object 名称不需要更改。
  2. Variant 允许应用程序在同一平台上加载不同的内容,但使用不同的硬件。
    • 这是支持各种移动设备的关键。iPhone 4 无法在任何实际应用程序中显示与最新 iPhone 相同的内容保真度。
    • 在 Android 上,AssetBundle Variants 可用于解决设备之间屏幕宽高比和 DPI 的巨大碎片问题。

1. 限制

AssetBundle Variant 系统的一个关键限制是它要求 Variants 从不同的 Assets 构建。即使这些资产之间的唯一差异是其导入设置,此限制也适用。如果构建到变体A 和变体B 中的纹理之间的唯一区别是在 Unity 纹理导入器中选择的特定纹理压缩算法,则变体A 和变体B 必须仍然是完全不同的资产。这意味着 Variant A 和 Variant B 必须是磁盘上的单独文件。

这种限制使大型项目的管理变得复杂,因为特定资产的多个副本必须保留在源代码管理中。当开发人员希望更改资产的内容时,必须更新资产的所有副本。此问题没有内置的解决方法。

大多数团队都实施自己的 AssetBundle Variants 形式。这是通过构建 AssetBundles 来完成的,并在其文件名后附加明确定义的后缀,以便识别给定 AssetBundle 所代表的特定 Variants。在构建这些 AssetBundle 时,自定义代码以编程方式更改所包含资产的导入器设置。一些开发人员已经扩展了他们的自定义系统,以便能够更改附加到预制件的组件的参数。

0×06 压缩或未压缩?

是否压缩 AssetBundle 需要考虑几个重要因素,其中包括:

  • 加载时间:从本地存储或本地缓存加载时,未压缩的 AssetBundle 加载速度比压缩的 AssetBundle 快得多。
  • 构建时间:压缩文件时 LZMA 和 LZ4 非常慢,Unity Editor 按顺序处理 AssetBundles。具有大量 AssetBundle 的项目将花费大量时间来压缩它们。
  • 应用程序大小:如果 AssetBundle 在应用程序中提供,压缩它们将减少应用程序的总大小。或者,可以在安装后下载 AssetBundle。
  • 内存使用:在 Unity 5.3 之前,所有 Unity 的解压缩机制都需要在解压缩之前将整个压缩的 AssetBundle 加载到内存中。如果内存使用很重要,请使用未压缩或 LZ4 压缩的 AssetBundle。
  • 下载时间:只有在 AssetBundle 较大或者用户处于带宽受限的环境中(例如在低速或计量连接上下载)时才可能需要压缩。如果在高速连接上只向 PC 传送几十兆字节的数据,则可以省略压缩。

1. 紧缩压缩

主要由使用 Crunch 压缩算法的 DXT 压缩纹理组成的捆绑包应该是未压缩的。

0×07 AssetBundles 和 WebGL

由于 Unity 的 WebGL 导出选项目前不支持工作线程,因此必须在主线程上进行所有 AssetBundle 在 WebGL 项目中的解压缩和加载。使用 XMLHttpRequest 将 AssetBundles 的下载委托给浏览器。下载后,压缩的 AssetBundle 将在 Unity 的主线程上解压缩,因此根据捆绑包的大小停止执行 Unity 内容。

Unity 建议开发人员更喜欢小型资产包,以避免产生性能问题。与使用大型资产包相比,此方法的内存效率更高。Unity WebGL 仅支持 LZ4 压缩和未压缩资产包,但是,可以对 Unity 生成的包应用 gzip / brotli 压缩。在这种情况下,您需要相应地配置 Web 服务器,以便浏览器在下载时解压缩文件。有关详细信息,请参见此处

如果您使用的是 Unity 5.5 或更早版本,请考虑为您的 AssetBundle 避免使用 LZMA,而使用 LZ4 进行压缩,这可以按需非常有效地解压缩。Unity 5.6 删除了 LZMA 作为 WebGL 平台的压缩选项

-EOF-