目录
AssetBundle
AssetBundle 是一个存档文件,包含可在运行时由 Unity 加载的特定于平台的非代码资源(比如模型、纹理、预制件(Pefabs)、音频剪辑甚至整个场景)。AssetBundle 可以表示彼此之间的依赖关系;例如,一个 AssetBundle 中的材质可以引用另一个 AssetBundle 中的纹理。为了提高通过网络传输的效率,可以根据用例要求(LZMA 和 LZ4)选用内置算法选择来压缩 AssetBundle。
AssetBundle 可用于可下载内容(DLC),减小初始安装大小,加载针对最终用户平台优化的资源,以及减轻运行时内存压力。
Build AssetBundle
Manifest Bundle
将会有一个额外的包和清单,它们的名称与您创建的任何 AssetBundle 都不相同。相反,它们的名称是根据它们所在的目录命名的(即生成 AssetBundles 的位置)。这是清单包(Manifest Bundle)。这个包包含 AssetBundleManifest
对象,该对象在运行时用于确定要加载的包依赖项。
AssetBundle 文件
这是缺少 .manifest 扩展名的文件,其中包含在运行时为了加载资源而需要加载的内容。
AssetBundle 文件是一个存档,在内部包含多个文件。此存档的结构根据它是 AssetBundle 还是 Scene AssetBundle 可能会略有不同。以下是普通 AssetBundle 的结构:
Scene AssetBundle 与普通 AssetBundle 的不同之处在于,它针对场景及其内容的串流加载进行了优化。
翻译:
Manifest File
对于每个生成的包,包括 Manifest Bundle ,都会生成一个关联的清单文件。清单文件的扩展名为 .manifest
,可以使用任何文本编辑器打开。它包含包的循环冗余校验(cyclic redundancy check, CRC)数据和依赖数据等信息。普通 AssetBundles 的清单文件看起来像这样:
ManifestFileVersion: 0
CRC: 2422268106
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: 8b6db55a2344f068cf8a9be0a662ba15
TypeTreeHash:
serializedVersion: 2
Hash: 37ad974993dbaa77485dd2a0c38f347a
HashAppended: 0
ClassTypes:
- Class: 91
Script: {instanceID: 0}
Assets:
Asset_0: Assets/Mecanim/StateMachine.controller
Dependencies: {}
其中显示了包含的资源、依赖项和其他信息。
也会为 Manifest Bundle 生成一个清单文件。它看起来像这样:
ManifestFileVersion: 0
AssetBundleManifest:
AssetBundleInfos:
Info_0:
Name: scene1assetbundle
Dependencies: {}
这个文件记录了 AssetBundles 之间的关系及其依赖项。它与清单包中的 AssetBundleManifest
对象记录的信息类似,并且由于它是一个文本文件,便于人类阅读和外部工具解析。
AssetBundle Dependencies
如果一个或多个 UnityEngine.Objects
包含对位于另一个捆绑包中的 UnityEngine.Object
的引用,则 AssetBundle 可以变为依赖于其他 AssetBundle。
如果 UnityEngine.Object
包含对任何 AssetBundle 中未包含的 UnityEngine.Object
的引用,则不会发生依赖关系。在这种情况下,在构建 AssetBundle 时,捆绑包所依赖的对象的副本将复制到捆绑包中。如果多个捆绑包中的多个对象包含对未分配给捆绑包的同一对象的引用,则每个对该对象具有依赖关系的捆绑包将创建其自己的对象副本并将其打包到构建的 AssetBundle 中。
如果 AssetBundle 中包含依赖项,则在加载尝试实例化的对象之前,务必加载包含这些依赖项的捆绑包。Unity 不会尝试自动加载依赖项。
AssetBundle 之间的重复信息
默认情况下,Unity 不会优化 AssetBundle 之间的重复信息。这意味着您项目中的多个 AssetBundle 可能包含相同信息,例如多个预制件使用的同一种材质。在多个 AssetBundle 中使用的资源被称为公共资源(common Assets)。这些会影响内存资源和加载时间。
示例
参考以下示例,Bundle 1 中的材质引用了 Bundle 2 中的纹理:
在此示例中,在从 Bundle 1 加载材质之前,需要将 Bundle 2 加载到内存中。加载 Bundle 1 和 Bundle 2 的顺序无关紧要,重要的是在从 Bundle 1 加载材质之前应加载 Bundle 2。
Editor 设置
默认情况下,Unity 不会执行任何优化措施来减少或最小化存储重复信息所需的内存。在创建构建版本期间,Unity 会在 AssetBundle 中对隐式引用的资源构建重复版本。 为避免发生此类重复,请将公共资源(例如材质)分配到它们自身的 AssetBundle。
例如:可能有一个应用程序包含两个预制件,这两个预制件都分配到它们自身的 AssetBundle。两个预制件共享相同材质,而该材质未分配到 AssetBundle。该材质引用一个纹理,而该纹理也未分配到 AssetBundle。
如果您遵循 AssetBundle 工作流程并使用示例类 CreateAssetBundles
来构建 AssetBundle,每个生成的 AssetBundle 都会包含此材质(包括其着色器和引用的纹理)。在以下示例图像中,预制件文件的大小分别为 383 KB 和 377 KB。
如果项目包含更多数量的预制件,此行为会影响最终安装程序的大小以及加载这两个 AssetBundle 时的运行时内存占用量。由于 Unity 将同一材质的每个副本视为独立材质,因此 AssetBundle 之间的数据重复问题也会影响批处理。
为了避免发生数据重复,请将材质及其引用的资源分配给其各自的 modulesmaterials
AssetBundle。还可以仅标记(tag)材质,因为构建过程中,纹理依赖关系也会被自动包含在 AssetBundle 中。
现在,如果重新构建 AssetBundle ,则生成的输出包含单独的 modulesmaterials
AssetBundle (359 KB),其中包含此材质及其关联的纹理。这会显著减小预制件的其他 AssetBundle 的大小(从上一个迭代的大约 380 KB 减小到大约 20 KB)。
下图说明了此情况。
运行时加载
将公共资源提取到单个 AssetBundle 时,请注意依赖关系。特别要注意的是,如果仅使用 Prefab 来实例化模块,则不会加载材质。
要解决此问题,请先在内存中加载 Material AssetBundle,然后再加载属于模块的 AssetBundle。在以下示例中,这个过程发生在变量 materialsAB
中。
using System.IO;
using UnityEngine;
public class InstantiateAssetBundles : MonoBehaviour
{
void Start()
{
var materialsAB = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, Path.Combine("AssetBundles", "modulesmaterials")));
var moduleAB = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, Path.Combine("AssetBundles", "example-prefab")));
if (moduleAB == null)
{
Debug.Log("Failed to load AssetBundle!");
return;
}
var prefab = moduleAB.LoadAsset<GameObject>("example-prefab");
Instantiate(prefab);
}
}
**注意:**使用 LZ4 压缩和未压缩的 AssetBundle 时,AssetBundle.LoadFromFile 仅在内存中加载其内容目录,而未加载内容本身。要检查是否发生了此情况,请使用内存性能分析器 (Memory Profiler) 包来检查内存使用情况。
Native AssetBundles
可以使用四种不同的 API 来加载 AssetBundle。它们的行为根据加载捆绑包的平台和构建 AssetBundle 时使用的压缩方法(未压缩、LZMA 和 LZ4)而有所不同。
我们必须使用的四个 API 是:
- AssetBundle.LoadFromMemoryAsync
- AssetBundle.LoadFromFile
- WWW.LoadfromCacheOrDownload
- UnityWebRequestAssetBundle 的 DownloadHandlerAssetBundle (Unity 5.3 或更高版本)
AssetBundle.LoadFromMemoryAsync
AssetBundle.LoadFromMemoryAsync
此函数采用包含 AssetBundle 数据的字节数组。也可以根据需要传递 CRC 值。如果捆绑包采用的是 LZMA 压缩方式,将在加载时解压缩 AssetBundle。LZ4 压缩包则会以压缩状态加载。
以下是如何使用此方法的一个示例:
using UnityEngine;
using System.Collections;
using System.IO;
public class Example : MonoBehaviour
{
IEnumerator LoadFromMemoryAsync(string path)
{
AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
yield return createRequest;
AssetBundle bundle = createRequest.assetBundle;
var prefab = bundle.LoadAsset<GameObject>("MyObject");
Instantiate(prefab);
}
}
但是,这不是实现 LoadFromMemoryAsync 的唯一策略。File.ReadAllBytes(path) 可以替换为获得字节数组的任何所需过程。
AssetBundle.LoadFromFile
AssetBundle.LoadFromFile
从本地存储中加载未压缩的捆绑包时,此 API 非常高效。如果捆绑包未压缩或采用了数据块 (LZ4) 压缩方式,LoadFromFile 将直接从磁盘加载捆绑包。使用此方法加载完全压缩的 (LZMA) 捆绑包将首先解压缩捆绑包,然后再将其加载到内存中。
如何使用 LoadFromFile
的一个示例:
using System.IO;
using UnityEngine;
public class LoadFromFileExample : MonoBehaviour
{
void Start()
{
var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));
if (myLoadedAssetBundle == null)
{
Debug.Log("Failed to load AssetBundle!");
return;
}
var prefab = myLoadedAssetBundle.LoadAsset<GameObject>("MyObject");
Instantiate(prefab);
}
}
UnityWebRequestAssetBundle
UnityWebRequestAssetBundle
UnityWebRequestAssetBundle 有一个特定 API 调用来处理 AssetBundle。首先,需要使用 UnityWebRequestAssetBundle.GetAssetBundle
来创建 Web 请求。返回请求后,请将请求对象传递给 DownloadHandlerAssetBundle.GetContent(UnityWebRequestAssetBundle)
。GetContent
调用将返回 AssetBundle 对象。
下载捆绑包后,还可以在 DownloadHandlerAssetBundle 类上使用 assetBundle
属性,从而以 AssetBundle.LoadFromFile
的效率加载 AssetBundle。
以下示例说明了如何加载包含两个游戏对象的 AssetBundle 并实例化这些游戏对象。要开始这个过程,我们只需要调用 StartCoroutine(InstantiateObject())
;
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequestAssetBundle request
= UnityEngine.Networking.UnityWebRequestAssetBundle.GetAssetBundle(uri, 0);
yield return request.SendWebRequest();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
Instantiate(cube);
Instantiate(sprite);
}
从 AssetBundle 加载资源
现在已经成功下载 AssetBundle,因此是时候最终加载一些资源了。
通用代码片段:
T objectFromBundle = bundleObject.LoadAsset<T>(assetName);
T 是尝试加载的资源类型。
决定如何加载资源时有几个选项。我们有 LoadAsset
、LoadAllAssets
及其各自的异步对应选项 LoadAssetAsync
和 LoadAllAssetsAsync
。
同步从 AssetBundle 加载资源的方法如下:
加载单个游戏对象:
GameObject gameObject = loadedAssetBundle.LoadAsset<GameObject>(assetName);
加载所有资源:
Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();
现在,在前面显示的方法返回要加载的对象类型或对象数组的情况下,异步方法返回 AssetBundleRequest。在访问资源之前,需要等待此操作完成。加载资源:
AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
yield return request;
var loadedAsset = request.asset;
以及
AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync();
yield return request;
var loadedAssets = request.allAssets;
加载资源后,就可以开始了!可以像使用 Unity 中的任何对象一样使用加载的对象。
加载 AssetBundle Manifests
加载 AssetBundle 清单可能非常有用。特别是在处理 AssetBundle 依赖关系时。
要获得可用的 AssetBundleManifest 对象,需要加载另外的 AssetBundle(与其所在的文件夹名称相同的那个)并从中加载 AssetBundleManifest 类型的对象。
加载清单本身的操作方法与 AssetBundle 中的任何其他资源完全相同:
AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
现在,可以通过上面示例中的清单对象访问 AssetBundleManifest
API 调用。从这里,可以使用清单获取所构建的 AssetBundle 的相关信息。此信息包括 AssetBundle 的依赖项数据、哈希数据和变体数据。
别忘了在前面的部分中,我们讨论过 AssetBundle 依赖项以及如果一个捆绑包对另一个捆绑包有依赖性,那么在从原始捆绑包加载任何资源之前,需要加载哪些捆绑包?清单对象可以动态地查找加载依赖项。假设我们想要为名为“assetBundle”的 AssetBundle 加载所有依赖项。
AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("assetBundle"); //传递想要依赖项的捆绑包的名称。
foreach(string dependency in dependencies)
{
AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));
}
现在已经加载 AssetBundle、AssetBundle 依赖项和资源,因此是时候讨论如何管理所有这些已加载的 AssetBundle 了。
管理已加载的 AssetBundle
注意: Addressable Assets 包提供了一个现成的系统来管理加载资源包、依赖项和资源。Unity 建议使用 Addressables 而不是自己管理 AssetBundles 。
从活跃场景(active scene)中删除对象时,Unity 不会自动卸载对象。资源清理在特定时间触发,也可以手动触发。
了解何时加载和卸载 AssetBundle 非常重要。不正确地卸载 AssetBundle 会导致在内存中复制对象或其他不良情况,例如缺少纹理。
关于 AssetBundle 管理的最需要了解的是何时调用AssetBundle.Unload(bool) – 或者AssetBundle.UnloadAsync(bool) – 以及应该将 true 还是 false 传递到函数调用中。
Unload 是一个非静态函数,可用于卸载 AssetBundle。此 API 会卸载正在调用的 AssetBundle 的标头信息。该参数指示是否还要卸载通过此 AssetBundle 实例化的所有对象。
AssetBundle.Unload(true)
卸载从 AssetBundle 加载的所有 GameObjects(及其依赖项)。这不包括复制的 GameObject(例如实例化的 GameObjects ),因为它们不再属于 AssetBundle。发生这种情况时,从该 AssetBundle 加载的纹理(并且仍然属于该 AB)会从场景中的游戏对象消失,因此 Unity 将它们视为缺少纹理。
假设 Material M 是从 AssetBundle AB 如下所示加载并在 Prefab P 中使用。
如果调用 AB.Unload(true),活跃场景中的任何包含 M 的实例也将被卸载并销毁。
如果改作调用 AB.Unload(false),那么将会中断 M 和 AB 当前实例的链接关系。
如果稍后再次加载 AB 并且调用 AB.LoadAsset(),则 Unity 不会将现有 M 副本重新链接到新加载的材质。
如果您创建另一个 Prefab P 的实例,它将不会使用 M 的现有副本。而是加载 M 的两个副本。
通常,使用 AssetBundle.Unload(false)
不会获得理想的情况。大多数项目应该使用 AssetBundle.Unload(true)
并采用一种方法来确保对象不会重复。两种常用方法是:
- 在应用程序生命周期中具有明确定义的卸载瞬态 AssetBundle 的时间点,例如在关卡之间或在加载屏幕期间。
- 维护单个对象的引用计数,仅当未使用所有组成对象时才卸载 AssetBundle。这允许应用程序卸载和重新加载单个对象,而无需复制内存。
如果应用程序必须使用 AssetBundle.Unload(false)
,则只能以两种方式卸载单个对象:
- 在场景和代码中消除对不需要的对象的所有引用。完成此操作后,调用 Resources.UnloadUnusedAssets。
- 以非附加(non-additively)方式加载场景。这样会销毁当前场景中的所有对象并自动调用 Resources.UnloadUnusedAssets。
分组策略
逻辑实体分组
逻辑实体分组是指根据资源所代表的项目功能部分将资源分配给 AssetBundle。这包括各种不同部分,比如用户界面、角色、环境以及在应用程序整个生命周期中可能经常出现的任何其他内容。
示例
- 捆绑用户界面屏幕的所有纹理和布局数据
- 捆绑一个/一组角色的所有模型和动画
- 捆绑在多个关卡之间共享的景物的纹理和模型
逻辑实体分组非常适合于可下载内容 (DLC),因为通过这种方式将所有内容隔离后,可以对单个实体进行更改,而无需下载额外的未更改的资源。
为了能够正确实施此策略,最大诀窍在于,负责为各自捆绑包分配资源的开发人员必须熟悉项目使用每个资源的准确时机和场合。
类型分组
根据此策略,可以将相似类型的资源(例如音频轨道或语言本地化文件)分配到单个 AssetBundle。
要构建供多个平台使用的 AssetBundle,类型分组是最佳策略之一。例如,如果音频压缩设置在 Windows 和 Mac 平台上完全相同,则可以自行将所有音频数据打包到 AssetBundle 并重复使用这些捆绑包,而着色器往往使用更多特定于平台的选项进行编译,因此为 Mac 构建的着色器捆绑包可能无法在 Windows 上重复使用。此外,这种方法非常适合让 AssetBundle 与更多 Unity player 版本兼容,因为纹理压缩格式和设置的更改频率低于代码脚本或预制件。
并发内容分组
并发内容分组是指将需要同时加载和使用的资源捆绑在一起。可以将这些类型的捆绑包用于基于关卡的游戏(其中每个关卡包含完全独特的角色、纹理、音乐等)。有时可能希望确保其中一个 AssetBundle 中的资源与该捆绑包中的其余资源同时使用。依赖于并发内容分组捆绑包中的单个资源会导致加载时间显著增加。您将被迫下载该单个资源的整个捆绑包。
并发内容分组捆绑包最常见的用例是针对基于场景的捆绑包。在此分配策略中,每个场景捆绑包应包含大部分或全部场景依赖项。
总结
请注意,项目绝对可以也应该根据您的需求混用这些策略。对任何给定情形使用最优资源分配策略可以大大提高项目的效率。
例如,一个项目可能决定将不同平台的用户界面 (UI) 元素分组到各自的 Platform-UI 特定捆绑包中,但按关卡/场景对其交互式内容进行分组。
无论遵循何种策略,下面这些额外提示都有助于掌控全局:
- 将频繁更新的对象与很少更改的对象拆分到不同的 AssetBundle 中
- 将可能同时加载的对象分到一组。例如模型及其纹理和动画
- 如果发现多个 AssetBundle 中的多个对象依赖于另一个完全不同的 AssetBundle 中的单个资源,请将依赖项移动到单独的 AssetBundle。如果多个 AssetBundle 引用其他 AssetBundle 中的同一组资源,一种有价值的做法可能是将这些依赖项拉入一个共享 AssetBundle 来减少重复。
- 如果不可能同时加载两组对象(例如标清资源和高清资源),请确保它们位于各自的 AssetBundle 中。
- Consider splitting apart an AssetBundle if less than 50% of that bundle is ever frequently loaded at the same time
- 如果一个 Asset Bundle 中需要频繁同时加载的内容比例小于 50%,考虑将其拆分
- 考虑将多个小型的(少于 5 到 10 个资源)但经常同时加载内容的 AssetBundle 组合在一起
- 如果一组对象只是同一对象的不同版本,请考虑使用 AssetBundle 变体。
AssetBundle compression
AssetBundle 压缩格式
默认情况下,Unity 通过 LZMA 压缩来创建 AssetBundle,然后通过 LZ4 压缩将其缓存。本部分描述这两种压缩格式。
Unity 的 AssetBundle 构建管道使用 LZMA 压缩创建 AssetBundle。这种压缩格式是一种表示整个 AssetBundle 的数据流,这意味着如果需要从这些归档中读取某个资源,则必须解压缩整个数据流。这是从内容分发网络 (CDN) 下载 AssetBundle 的首选格式,因为与 LZ4 压缩相比,文件大小更小。
LZ4 压缩是一种基于块的压缩算法。如果 Unity 需要从 LZ4 归档中访问资源,它只需解压缩和读取包含所请求资源字节的块。这是 Unity 在其两个 AssetBundle 缓存中使用的压缩方法。构建 AssetBundle 时使用 BuildAssetBundleOptions.ChunkBasedCompression
值可以强制使用 LZ4(HC) 压缩。
使用 BuildAssetBundleOptions.UncompressedAssetBundle
构建的未压缩 AssetBundle 无需解压缩,但占用更多磁盘空间。未压缩的 AssetBundle 是 16 字节对齐的。
AssetBundle Cache
为了优化使用 WWW 或 UnityWebRequest (UWR) 获取、重新压缩和版本化 LZMA AssetBundle,Unity 维护了两个缓存:
- 内存缓存:将 AssetBundle 以 UncompressedRuntime 格式存储在 RAM 中。
- 磁盘缓存:将提取的 AssetBundle 以本文描述的压缩格式存储在可写介质上。
将 AssetBundle 加载到内存缓存中会耗用大量内存。除非您特别希望频繁且快速地访问 AssetBundle 的内容,否则内存缓存的性价比可能不高。因此,应改用磁盘缓存。
如果您向 UWR API 提供版本参数(version parameter),Unity 会将您的 AssetBundle 数据存储在磁盘缓存中。如果没有提供版本参数,Unity 将使用内存缓存。版本参数可以是版本号或哈希值。如果 Caching.compressionEnabled
设置为 true,Unity 会在将 AssetBundle 写入磁盘时应用 LZ4 压缩(针对所有后续下载)。它不会压缩缓存中现有的未压缩数据。如果 Caching.compressionEnabled
为 false,Unity 在将 AssetBundle 写入磁盘时不会应用任何压缩。
最初加载缓存的 LZMA AssetBundle 所花费的时间更长,因为 Unity 必须将存档重新压缩为目标格式。随后的加载将使用缓存版本。
AssetBundle.LoadFromFile
或 AssetBundle.LoadFromFileAsync
对于 LZMA AssetBundle 总是使用内存缓存,因此开发者应该使用 UWR API。如果无法使用 UWR API,您可以使用 AssetBundle.RecompressAssetBundleAsync
重新写入磁盘上的 LZMA AssetBundle。
注意:WebGL 不支持 LZMA 压缩的 AssetBundle。请在 WebGL 平台上使用 LZ4 压缩的 AssetBundle。有关更多信息,请参阅 使用 AssetBundle 减少加载时间。
内部测试表明,使用磁盘缓存而不是内存缓存,在 RAM 使用量上有显著差异。开发者必须权衡内存影响与增加的磁盘空间需求和资源实例化时间之间的权衡,以适应自己的应用程序。
BuildAssetBundleOptions
可以指定几个具有各种效果的不同 BuildAssetBundleOptions
。请参阅关于 BuildAssetBundleOptions 的脚本 API 参考查看所有这些选项的表格。
虽然可以根据需求变化和需求出现而自由组合 BuildAssetBundleOptions
,但有三个特定的 BuildAssetBundleOptions
可以处理 AssetBundle 压缩:
BuildAssetBundleOptions.None
:此捆绑包选项使用 LZMA 格式压缩,这是一个压缩的 LZMA 序列化数据文件流。LZMA 压缩要求在使用捆绑包之前对整个捆绑包进行解压缩。此压缩使文件大小尽可能小,但由于需要解压缩,加载时间略长。值得注意的是,在使用此 BuildAssetBundleOptions 时,为了使用捆绑包中的任何资源,必须首先解压缩整个捆绑包。
解压缩捆绑包后,将使用 LZ4 压缩技术在磁盘上重新压缩捆绑包,这不需要在使用捆绑包中的资源之前解压缩整个捆绑包。最好在包含资源时使用,这样,使用捆绑包中的一个资源意味着将加载所有资源。这种捆绑包的一些用例是打包角色或场景的所有资源。
由于文件较小,建议仅从异地主机初次下载 AssetBundle 时才使用 LZMA 压缩。通过 UnityWebRequestAssetBundle 加载的 LZMA 压缩格式 Asset Bundle 会自动重新压缩为 LZ4 压缩格式并缓存在本地文件系统上。如果通过其他方式下载并存储捆绑包,则可以使用 AssetBundle.RecompressAssetBundleAsync API 对其进行重新压缩。BuildAssetBundleOptions.UncompressedAssetBundle
:此捆绑包选项采用使数据完全未压缩的方式构建捆绑包。未压缩的缺点是文件下载大小增大。但是,下载后的加载时间会快得多。未压缩的 AssetBundles 是 16 字节对齐的。BuildAssetBundleOptions.ChunkBasedCompression
:此捆绑包选项使用称为 LZ4 的压缩方法,因此压缩文件大小比 LZMA 更大,但不像 LZMA 那样需要解压缩整个包才能使用捆绑包。LZ4 使用基于块的算法,允许按段或“块”加载 AssetBundle。解压缩单个块即可使用包含的资源,即使 AssetBundle 的其他块未解压缩也不影响。
总结
- LZMA:适用于需要高压缩比的场景,如初次下载和离线存储,但解压速度较慢。
- LZ4:适用于需要快速解压的场景,如实时加载和频繁访问的资源,压缩比略低但速度快。
修补(Patching With) AssetBundle
修补 AssetBundle 很简单,只需要下载新的 AssetBundle 并替换现有的 AssetBundle。如果使用 WWW.LoadFromCacheOrDownload
或 UnityWebRequest
来管理应用程序的缓存 AssetBundle,则将不同的版本参数传递给所选 API 将触发新 AssetBundle 的下载。
在修补系统(patching system)中要解决的更难的问题是检测要替换的 AssetBundle。修补系统需要两个信息列表:
- 当前已下载的 AssetBundle 及其版本控制信息的列表
- 服务器上的 AssetBundle 及其版本控制信息的列表
修补程序应下载服务器端 AssetBundle 列表并比较这些 AssetBundle 列表。应重新下载缺少的 AssetBundle 或已更改版本控制信息的 AssetBundle。
也可以编写一个自定义系统来检测 AssetBundle 的更改。自己编写系统的大多数开发人员会选择对 AssetBundle 文件列表使用行业标准数据格式(例如 JSON)和并使用标准 C# 类(例如 MD5)来计算校验和。
Unity 使用以确定方式排序的数据构建 AssetBundle。因此,具有自定义下载程序的应用程序可以实现差异修补(differential patching)。
Unity 不提供任何内置的差异修补机制,并且 WWW.LoadFromCacheOrDownload
和 UnityWebRequest
在使用内置缓存系统时都不会执行差异修补。如果需要差异修补,则必须编写自定义下载程序。
故障排除
本部分将介绍使用 AssetBundle 的项目中常见的几个问题。
资源重复
Unity 的 AssetBundle 系统会在对象被构建到 AssetBundle 中时,发现对象的所有依赖项。这是通过**资源数据库(Asset Database)**完成的。这些依赖信息用于确定将包含在 AssetBundle 中的对象集合。
资源分配给 AssetBundle 是在资源级别进行的。显式分配给某个 AssetBundle 的资源内部的对象只会构建到该单个 AssetBundle 中。根据调用 BuildPipeline.BuildAssetBundles 的不同签名,资源是通过将资源的 AssetImporter.assetBundleName 属性设置为非空字符串,或者通过在 AssetBundleBuild.assetNames 中列出它来“显式分配”的。
任何未显式分配在某个 AssetBundle 中的作为资源的一部分的对象,将会被包含在任何引用它们的对象所在的 AssetBundle 中。
换句话说,如果两个不同的对象被分配给两个不同的 AssetBundle ,但它们都有对共同依赖对象的引用,那么该依赖对象将被复制到这两个 AssetBundle 中。重复的依赖项也会实例化,这意味着依赖对象的两个副本将被视为不同的对象,具有不同的标识符。这会增加应用程序的 AssetBundle 的总大小。如果应用程序同时加载这两个父对象,还会导致内存中加载两个不同的副本。
有几种方法可以解决这个问题:
-
确保构建到不同 AssetBundle 中的对象不共享依赖项。任何共享依赖项的对象都可以放在同一个 AssetBundle 中,而不会复制它们的依赖项。
- 对于具有许多共享依赖项的项目,此方法通常不可行。这种情况下可能生成单独的 AssetBundle,必须频繁进行重建和重新下载,因此很不方便或高效。
-
对 AssetBundle 进行分段,确保不会同时加载两个共享依赖项的 AssetBundle。
- 此方法可能适用于某些类型的项目,例如基于关卡的游戏。然而,这仍然存在权衡,因为重复的对象会增加项目 AssetBundle 的构建时间和大小,并可能影响加载时间。
-
确保所有依赖项资源都 build 到自己的 AssetBundle 中。这样可以完全消除重复资源的风险,但也带来了复杂性。应用程序必须跟踪 AssetBundle 之间的依赖关系,并确保在调用任何
AssetBundle.LoadAsset
API 之前加载了正确的 AssetBundle。
对象依赖项通过位于 UnityEditor
命名空间中的 AssetDatabase
API 进行跟踪。顾名思义,此 API 仅在 Unity 编辑器中可用,运行时不可用。可以使用 AssetDatabase.GetDependencies
查找特定对象或资源的所有直接依赖项。注意,这些依赖项可能有自己的依赖项,因此这可能是一个递归计算。资源分配给 AssetBundle 要么通过在调用 BuildPipeline.BuildAssetBundles
时传递 AssetBundleBuild
结构数组显式定义,要么可以通过 AssetImporter
API 查询。可以编写一个编辑器脚本,确保所有 AssetBundle 的直接或间接依赖项都被分配给 AssetBundle,或者确保没有两个 AssetBundle 共享未分配给某个 AssetBundle 的依赖项。
注意:使用 Addressables 包构建 AssetBundle 时,可以使用 Addressables Analyze 窗口发现重复的资源。
精灵图集(Sprite Atlas)重复
以下部分描述了在与自动生成的 Sprite Atlas 一起使用时,资源依赖计算代码的一个特性。
任何自动生成的精灵图集都会与生成精灵图集的精灵对象一起分配到同一个 AssetBundle。如果精灵对象被分配给多个 AssetBundle,则精灵图集将不会被分配给 AssetBundle 并且将被复制。如果精灵对象未分配给 AssetBundle,则精灵图集也不会分配给 AssetBundle。
为了确保精灵图集不重复,请确保标记到相同精灵图集的所有精灵都被分配到同一个 AssetBundle。
着色器与 AssetBundles 的交互
当你构建一个 AssetBundle 时,Unity 使用该包中的信息选择要编译的着色器变体(shader
**** variants)。这些信息包括 AssetBundle 中的场景、材质、图形设置和 ShaderVariantCollection
。
单独的构建管道(build pipeline)独立于其他管道来编译自己的着色器。如果一个玩家构建和一个 Addressables 构建都引用了一个着色器,Unity 会分别为每个管道编译两个独立的着色器副本。
这个过程不考虑任何运行时信息,如关键字、纹理或代码执行引起的变化。如果你想在构建中包含特定的着色器,可以包含一个 ShaderVariantCollection
来包含所需的着色器,或者通过将其添加到图形设置中的“始终包含的着色器”列表中来手动在构建中包含着色器。
Unity Asset Bundle Browser Tool
您可以在 Unity 项目中使用 Asset Bundle Browser 工具能够查看和编辑资源包的配置。
有关更多信息,请参阅 Unity Asset Bundle Browser 文档。
Unity Asset Bundle Browser 工具
该工具允许用户查看和编辑 Unity 项目中 AssetBundle 的配置。它会阻止会创建无效 Bundle 的编辑,并告知开发者现有 Bundle 的任何问题。它还提供了基本的构建功能。
使用此工具作为在检查器中手动选择资源并设置其 AssetBundle 的替代方法。它可以放入任何 Unity 5.6 或更高版本的项目中。它会在 Window > AssetBundle Browser
中创建一个新菜单项。Bundle 配置、构建功能和构建 Bundle 检查分为新窗口中的三个选项卡。
使用 - 配置
此窗口提供了一个类似资源管理器的界面,用于管理和修改项目中的 AssetBundle 。首次打开时,工具将在后台解析所有 Bundle 数据,并慢慢标记它检测到的警告或错误。它会尽可能与项目保持同步,但无法始终了解工具外的活动。要强制快速进行错误检测或更新工具以反映外部更改,请点击左上角的刷新按钮。
该窗口分为四个部分:Bundle 列表、Bundle 详细信息、资源列表和资源详细信息。
Bundle 列表
左侧窗格显示项目中所有 Bundle 的列表。可用功能:
-
选择一个或一组 Bundle 以在资源列表窗格中查看将包含在 Bundle 中的资源列表。
-
具有变体的 Bundle 显示为深灰色,并且可以展开以显示变体列表。
-
右键单击或慢速双击可重命名 Bundle 或 Bundle 文件夹。
-
如果 Bundle 有任何错误、警告或信息消息,图标会显示在右侧。鼠标悬停在图标上可查看更多信息。
-
如果 Bundle 中包含至少一个场景(使其成为场景 Bundle)和非场景资源,则会被标记为错误。该 Bundle 需要修复后才能构建。
-
具有重复资源的 Bundle 将被标记为警告(关于重复的更多信息见下文的资源列表部分)。
-
空的 Bundle 将被标记为 info 消息。由于多种原因,空 Bundle 不太稳定,有时会从列表中消失。
-
包含 Bundle 的文件夹将被标记为包含 Bundle 中最高的消息。
-
修复 Bundle 中资源重复的问题的方法:
- 右键单击单个 Bundle 以将所有被确定为重复的资源移动到新 Bundle 中。
- 右键单击多个 Bundle,可以选择将所有选定 Bundle 中的重复资源移动到新 Bundle,或仅移动在选择范围内共享的资源。
- 您也可以将重复的资源从资源列表窗格拖动到 Bundle 列表,以明确将它们包含在一个 Bundle 中。更多信息见下文的资源列表功能集。
-
右键单击或按 DEL 删除 Bundle。
-
拖动 Bundle 以将它们移动到文件夹内或文件夹外,或合并它们。
-
将资源从项目资源管理器拖到 Bundle 以添加它们。
-
将资源拖到空白处以创建一个新 Bundle。
-
右键单击以创建新 Bundle 或 Bundle 文件夹。
-
右键单击以“转换为变体”。
- 这会向选定的 Bundle 添加一个变体(初始名称为“newvariant”),并将当前选定 Bundle 中的所有资源移动到新变体中。
- 正在开发:变体之间的错配检测。
-
图标指示 Bundle 是标准还是场景 Bundle。
Bundle 详细信息
左下窗格显示在 Bundle 列表窗格中选定的 Bundle 的详细信息。如果可用,此窗格将显示以下信息:
- Bundle 的总大小。这是所有资源的磁盘大小总和。
- 当前 Bundle 所依赖的 Bundle。
- 与当前 Bundle 相关的任何消息(错误/警告/信息)。
资源列表
右上窗格提供包含在选定 Bundle 中的资源列表。搜索字段将匹配任何 Bundle 中的资源。资源列表只显示匹配的资源,Bundle 列表只显示包含匹配资源的 Bundle。可用功能:
-
查看预计包含在 Bundle 中的所有资源。按任意列标题对资源列表进行排序。
-
查看明确包含在 Bundle 中的资源。这些是明确分配了 Bundle 的资源。检查器将反映 Bundle 包含情况,在此视图中它们的名称旁边将显示 Bundle 名称。
-
查看隐含包含在 Bundle 中的资源。这些资源的名称旁边会显示
auto
作为 Bundle 名称。如果在检查器中查看这些资源,它们将显示为未分配 Bundle。- 这些资源由于依赖于其他包含的资源而被添加到选定的 Bundle 中。只有未明确分配到 Bundle 的资源才会隐含包含在任何 Bundle 中。
- 请注意,此隐含包含列表可能不完整。已知在某些情况下,材质和纹理不会正确显示。
- 由于多个资源可以共享依赖项,通常一个给定资源会被隐含包含在多个 Bundle 中。如果工具检测到这种情况,它会在相关的 Bundle 和资源上标记一个警告图标。
- 要修复重复包含警告,可以手动将资源移动到新 Bundle 中,或右键单击 Bundle 并选择“移动重复项”选项之一。
-
将资源从项目资源管理器拖动到此视图以将其添加到选定的 Bundle 中。这仅在选中一个 Bundle 时有效,并且资源类型兼容(例如场景资源拖动到场景 Bundle 中)。
-
将资源(显式或隐含)从资源列表拖动到 Bundle 列表中(以将它们添加到不同的 Bundle 或新创建的 Bundle 中)。
-
右键单击或按 DEL 从 Bundle 中删除资源(不会从项目中删除资源)。
-
选择或双击资源以在项目资源管理器中显示它们。
关于在 Bundle 中包含文件夹的说明。可以将一个资源文件夹(从项目资源管理器)分配到一个 Bundle。当在浏览器中查看时,文件夹本身将列为显式包含,而内容为隐含包含。这反映了用于将资源分配到 Bundle 的优先级系统。例如,假设您的游戏在 Assets/Prefabs
中有五个预制件,并且您将文件夹 Prefabs
标记为一个 Bundle,并将其中一个实际预制件 (PrefabA
) 标记为另一个 Bundle。构建后,PrefabA
会在一个 Bundle 中,其他四个预制件会在另一个 Bundle 中。
资源详细信息
右下窗格显示在资源列表窗格中选定的资源的详细信息。此窗格无法交互,但会显示以下信息(如果可用):
- 资源的完整路径。
- 如果资源是隐含包含的,显示其在 Bundle 中隐含包含的原因。
- 显示警告的原因(如果有)。
- 显示错误的原因(如果有)。
故障排除
无法重命名或删除特定 Bundle。这有时是因为首次将此工具添加到现有项目时导致的。请通过 Unity 菜单系统强制重新导入您的资源以刷新数据。
外部工具集成
其他生成 AssetBundle 数据的工具可以选择与浏览器集成。目前的主要示例是 Asset Bundle Graph Tool。如果检测到集成,浏览器顶部附近会出现一个选择栏。它允许您选择默认数据源(Unity 的 AssetDatabase)或集成工具。如果没有检测到集成,则不会显示选择器,但您可以通过右键单击选项卡标题并选择“自定义源”来添加它。
使用 - 构建
构建选项卡提供基本的构建功能,帮助您开始使用 AssetBundle。在大多数专业场景中,用户最终需要更高级的构建设置。所有用户都可以将此工具中的构建代码作为编写自己的构建代码的起点,一旦不再满足他们的需求。大多数情况下,这里的选项直接与引擎在 BuildAssetBundleOptions
中期望的选项相关联。界面:
-
构建目标:构建 Bundle 的平台。
-
输出路径:保存构建 Bundle 的路径。默认情况下是
AssetBundles/
。您可以手动编辑路径,或选择“浏览”。要返回到默认命名约定,请点击“重置”。 -
清除文件夹:在构建之前删除构建路径文件夹中的所有数据。
-
复制到 StreamingAssets:构建完成后,将结果复制到
Assets/StreamingAssets
。这对于测试很有用,但不适用于生产环境。 -
高级设置
- 压缩:选择无压缩、标准 LZMA 或基于块的 LZ4 压缩。
- 排除类型信息:不要在 AssetBundle 中包含类型信息。
- 强制重建:重建需要构建的 Bundle。与“清除文件夹”不同,此选项不会删除不再存在的 Bundle。
- 忽略类型树更改:在增量构建检查时忽略类型树更改。
- 附加哈希:在 AssetBundle 名称后附加哈希。
- 严格模式:如果在构建期间报告了任何错误,则不允许构建成功。
- 干运行构建(Dry Run Build):执行干运行构建。
-
构建:执行构建。
使用 - 检查
此选项卡允许您检查已构建的 Bundle 的内容。
使用方法
-
如果使用浏览器进行构建,则构建路径会自动添加到此处。
-
点击“添加文件”或“添加文件夹”以添加要检查的 Bundle。
-
点击每行旁边的“-”以删除该文件或文件夹。请注意,无法删除通过添加文件夹添加的单个文件。
-
选择列表中的任何 Bundle 以查看详细信息:
- 名称
- 磁盘上的大小
- 源资源路径 - 明确添加到该 Bundle 的资源。请注意,对于场景 Bundle,此列表不完整。
- 高级数据 - 包括预加载表、容器(显式资源)和依赖项的信息。
AssetBundle 下载的完整性(Integrity)和安全性(Security)
AssetBundle 可以与开发者的游戏构建一起分发,但也可以从远程服务器下载。下载 AssetBundle 时,开发者应该采取预防措施以防止 AssetBundle 数据损坏以及恶意行为者的攻击。尽管 AssetBundle 不能包含可执行代码,但更改序列化数据可能允许攻击者利用游戏代码或 Unity 运行时中的漏洞。
采用安全协议(Secure Protocol)下载
UnityWebRequestAssetBundle 可以用于从互联网上下载和缓存 AssetBundles 。在使用此 API 时,开发者应在 URL 中使用 HTTPS 协议,除非开发者的 URL 指的是运行在同一台机器上的本地 Web 服务器。HTTP 协议是不安全的,容易受到恶意的中间人攻击。
CRC 校验和
在 AssetBundle 构建过程中将生成 32 位校验和。当您通过 AssetBundle 加载 API 提供此 CRC 时,加载系统会在加载之前计算 AssetBundle 的校验和。如果 AssetBundle 的 CRC 与提供的 CRC 不匹配,则不会加载 AssetBundle。检查 CRC 可确保 AssetBundle 数据在构建后未被损坏或篡改。
用户生成的内容
如果您允许用户上传分发给其他玩家的内容(用户生成的内容),则您有责任过滤此数据中的不当或恶意内容。Unity 不建议您让用户构建和上传二进制 AssetBundle 文件。最好让用户上传他们的源资源,并让开发者为他们构建 AssetBundle 二进制文件。这将使您更容易通过手动和自动流程过滤掉恶意或不适当的内容。如果您升级到更高的 Unity 版本,这样还可以让您根据需要重建 AssetBundle 。
Reference
- https://docs.unity3d.com/2022.3/Documentation/ScriptReference/BuildAssetBundlesParameters.html
- https://zhuanlan.zhihu.com/p/496737920
- Unity 学习教程 -管理已加载的 AssetBundle (Managing Loaded AssetBundles)
- https://github.com/wechat-miniprogram/minigame-unity-webgl-transform/blob/main/Design/UsingAssetBundle.md
- https://docs.unity3d.com/Manual/AssetBundlesIntro.html