文章目录
AssetBundle 的定义和作用
什么是 AssetBundle
-
它是一个存在于硬盘上的文件。可以称之为压缩包。这个压缩包可以认为是一个文件夹,里面包含了多个文件。这些文件可以分为两类:serialized file 和 resource files。(序列化文件和源文件)
-
serialized file:资源被打碎放在一个对象中,最后统一被写进一个单独的文件
- serialized file 只有一个
- 不能直接看到是什么东西,只有在游戏中加载才能看到的会被打包为序列化文件
- 例如:prefab,材质
-
resource files:为某些资源(图片和声音)单独存储的二进制数据块,方便快速加载
-
它是一个 AssetBundle 对象,我们可以通过代码从一个特定的压缩包加载出来的对象。这个对象包含了所有我们当初添加到这个压缩包里面的内容,我们可以通过这个对象加载出来使用。即:通过对象使用里面的资源
用处
- AssetBundle 是一个压缩包包含模型、贴图、预制体、声音、甚至整个场景,可以在游戏运行的时候被加载;
- AssetBundle 自身保存着互相的依赖关系;
- 例如一个包保存着模型,一个包保存着贴图,而模型又依赖于贴图
- 压缩包可以使用 LZMA 和 LZ4 压缩算法,可以减少包大小,更快的进行网络传输;
- 把一些可以下载内容(DLC)放在 AssetBundle 里面,减小初始安装大小,实时更新一些资源;
- 既然可以把资源放在 AB 里,那么就不会放在 apk 里,玩家下载安装包的时候就会很小,当玩家运行游戏的时候再在服务器上去下载资源包
- 可以更新一些资源而不用重新安装,例如:在特殊节日的时候更换不同的封面
AssetBundle 使用流程图
指定资源的 AssetBundle 属性
在 Inspector 底部,有 AssetBundle 的面板。
- AssetBundle 名称“environment/forest”表示,将在 environment 子文件夹下创建名为 forest 的捆绑包
- 名字不区分大小写,默认小写
- 指定相同名字的资源会被打到一个包里面
- 可以自行设置文件的后缀名
构建 AssetBundle 包
在 Assets 文件夹中创建一个名为 Editor 的文件夹,并将包含以下内容的脚本放在该文件夹中:
- 此脚本将在 Assets 菜单底部创建一个名为“Build AssetBundles”的菜单项,该菜单项将执行与该标签关联的函数中的代码。
- 单击 Build AssetBundles 会将带有 AssetBundle 名称标签的所有资源,打包放在指定目录下
using System.IO;
using UnityEditor;
public class CreateAssetBundles {
[MenuItem("Assets/Build AssetBundles")]
static void BuildAllAssetBundles() {
string assetBundleDirectory = "Assets/AssetBundles"; // 包的输出路径
if (!Directory.Exists(assetBundleDirectory)) { // 若路径不存在,则创建
Directory.CreateDirectory(assetBundleDirectory);
}
// BuildPipeline:允许您以编程方式构建可从 Web 加载的播放器或 AssetBundle。
// BuildAssetBundles():打包,Build 出来的包是有平台限制的
BuildPipeline.BuildAssetBundles(assetBundleDirectory, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
}
}
核心代码详解
public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
- Build 的路径:随意只要是在硬盘上都可以的
- BuildAssetBundleOptions
- None:使用 LZMA 算法压缩,压缩的包更小,但是加载时间更长。
- 使用之前需要整体解压,一旦被解压,这个包会使用 LZ4 重新压缩。
- LZ4 使用资源的时候不需要整体解压。
- 在下载的时候可以使用 LZMA 算法,一旦它被下载了之后,它会使用 LZ4 算法保存到本地上。(Unity 内置的优化策略)
- UncompressedAssetBundle:不压缩,包大,加载快
- ChunkBasedCompression:使用 LZ4 压缩,压缩率没有 LZMA 高,但是我们可以加载指定资源而不用解压全部。
- LZ4 使用基于块的算法,允许按段或“块”加载 AssetBundle。解压缩单个块即可使用包含的资源,即使 AssetBundle 的其他块未解压缩也不影响。
- 注意使用 LZ4 压缩,可以获得可以跟不压缩想媲美的加载速度,而且比不压缩文件要小。
- None:使用 LZMA 算法压缩,压缩的包更小,但是加载时间更长。
- BuildTarget:选择 build 出来的 AB 包要使用的平台
关于构建 AB 后产生的文件
[构建 AB 后产生的文件](#构建 AB 后产生的文件)
上传 AB 包
将资源包上传到自己的服务器,以供游戏运行时的加载。
加载 AB 包和包里面的资源
开发的时候一般会将 AB 包放在本地,因为会进行频繁的操作,发布的时候才会上传到服务器上。
加载本地的 AB 包
using System.IO;
using UnityEngine;
public class LoadFromFileExample : MonoBehaviour {
private void Start() {
AssetBundle ab = AssetBundle.LoadFromFile("Assets/AssetBundles/scene/cubewall.u3d");
if (ab == null) {
Debug.Log("Failed to load AssetBundle!");
return;
}
// 加载包里的指定物体
var wallPre = ab.LoadAsset<GameObject>("CubeWall");
Instantiate(wallPre); // 实例化物体
}
// 加载包里的所有物体
private void LoadAllAssets(AssetBundle ab) {
Object[] objects = ab.LoadAllAssets();
foreach (var o in objects) {
Instantiate(o);
}
}
}
加载服务器上的 AB 包
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
// GetAssetBundle(string, int):获取 AssetBundle 的位置以及要下载的捆绑包的版本。
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
Instantiate(cube);
Instantiate(sprite);
}
其他详细使用可见:
[AssetBundles 的具体使用](#AssetBundles 的具体使用 )
AssetBundle 分组策略
应根据实际项目的需求来进行设置,这里只是给一个简单的参考。
这里的分组策略不是指如何划分文件夹,而是,将哪些资源打包在一个 AssetBundle 里,一个 AssetBundle 就是一组。
按照逻辑实体分组
逻辑实体分组非常适合于可下载内容 (DLC),因为通过这种方式将所有内容隔离后,可以对单个实体进行更改,而无需下载其他未更改的资源。
- 一个 UI 界面或者所有 UI 界面一个包(这个界面里面的贴图和布局信息一个包)
- 一个角色或者所有角色一个包(这个角色里面的模型和动画一个包)
- 所有的场景所共享的部分一个包(包括贴图和模型)
按照资源类型分组
要构建供多个平台使用的 AssetBundle,类型分组是最佳策略之一。
例如,如果音频压缩在 Windows 和 Mac 平台上完全相同,则可以将所有音频数据打包到 AssetBundle 并重复使用这些捆绑包;而着色器往往使用更多特定于平台的选项进行编译,因此为 Mac 构建的着色器捆绑包可能无法在 Windows 上重复使用。此外,这种方法非常适合让 AssetBundle 与更多 Unity 播放器版本兼容,因为纹理压缩格式和设置的更改频率低于代码脚本或预制件。
- 所有声音资源打成一个包,所有 shader 打成一个包,所有模型打成一个包,所有材质打成一个包
按照使用分组
- 将需要同时加载和使用的资源捆绑在一起
- 可以按照关卡分,一个关卡所需要的所有资源包括角色、贴图、声音等打成一个包。
- 也可以按照场景分,一个场景所需要的资源一个包
总结
- 把经常更新的资源放在一个单独的包里面,同不经常更新的包分离
- 把需要同时加载的资源放在一个包里面
- 如果一个 AssetBundle 中只有不到 50% 的资源经常同时加载,请考虑拆分该捆绑包
- 如果不可能同时加载两组对象(例如标清资源和高清资源),请确保它们位于各自的 AssetBundle 中。
- 可以把其他包共享的资源放在一个单独的包里面
- 防止资源的重复打包
- 把一些需要同时加载的小资源打包成一个包
- 如果对于一个同一个资源有两个版本,可以考虑通过后缀来区分版本,xx.v1 xx.v2 xx.v3
构建 AB 后产生的文件
AssetBundle 文件
文件使用自定义后缀,包含在运行时为了加载资源而需要加载的内容。
此包的结构根据它是 AssetBundle 还是场景 AssetBundle 可能会略有不同。
-
普通 AssetBundle 的结构:
-
场景 AssetBundle 与普通 AssetBundle 的不同之处在于,它针对场景及其内容的串流加载进行了优化。
Manifest 清单文件
对于生成的每个 AB 包,都会生成关联的清单文件。清单文件包含诸如 循环冗余校验 (CRC) 数据 和 包的依赖性数据 等信息。
对于普通 AssetBundle,它们的清单文件将如下所示:
AssetBundles.manifest - AB 包的清单文件
ManifestFileVersion: 0
CRC: 4225903359
AssetBundleManifest:
AssetBundleInfos:
Info_0:
Name: share.u3d
Dependencies: {}
Info_1:
Name: cubewall.u3d
Dependencies:
Dependency_0: share.u3d
Info_2:
Name: spherewall.u3d
Dependencies:
Dependency_0: share.u3d
share.u3d.manifest - 包含贴图资源
ManifestFileVersion: 0
CRC: 4044919538
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: db0f6906b386d5d4413b2ddd1d9a6c61
TypeTreeHash:
serializedVersion: 2
Hash: 6f165f44e4778b6c9d85e7a145a54cb1
HashAppended: 0
ClassTypes:
- Class: 21
Script: {instanceID: 0}
- Class: 28
Script: {instanceID: 0}
- Class: 48
Script: {instanceID: 0}
Assets:
- Assets/Materials/Stone_floor_09.png
- Assets/Materials/Stone_floor_09.mat
Dependencies: []
cubewall.u3d.manifest - 依赖于 share
ManifestFileVersion: 0
CRC: 2862441256
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: 6e48f6b8e6cedd4070323880d839a3ee
TypeTreeHash:
serializedVersion: 2
Hash: f49f05f36a566d50434f7d9f3fb347da
HashAppended: 0
ClassTypes:
- Class: 1
Script: {instanceID: 0}
- Class: 4
Script: {instanceID: 0}
- Class: 21
Script: {instanceID: 0}
- Class: 23
Script: {instanceID: 0}
- Class: 33
Script: {instanceID: 0}
- Class: 43
Script: {instanceID: 0}
- Class: 65
Script: {instanceID: 0}
Assets:
- Assets/Prefabs/CubeWall.prefab
Dependencies:
- G:/UnityDocuments/AssetBundle/Assets/AssetBundles/share.u3d
AB 依赖
依赖打包
把共享资源放在一个包里,以节约空间。
加载依赖
如果 AssetBundle 中包含依赖项,则在加载尝试实例化的对象之前,务必加载包含这些依赖项的AB包。Unity 不会自动加载依赖项。
参考以下示例,a 中的材质引用了 b 中的纹理,加载 a 和 b 的顺序无关紧要,重要的是在使用 a 中的材质前应加载 b。
在此示例中,在从 a 加载材质之前,需要将 b 加载到内存中。加载 a 和 b 的顺序无关紧要,重要的是在使用 a 中的材质前应加载 b。
private void Start() {
AssetBundle.LoadFromFile("Assets/AssetBundles/share.u3d"); // 加载依赖包
AssetBundle ab = AssetBundle.LoadFromFile("Assets/AssetBundles/cubewall.u3d");
var wallPre = ab.LoadAsset<GameObject>("CubeWall");
Instantiate(wallPre); // 实例化物体
}
[当 AB 包存在依赖关系时,如何知道在加载原始包的资源前,需要加载哪些依赖包?](#加载 AssetBundle 清单)
AssetBundles 的具体使用
加载 AB
根据 AssetBundle 是以什么样的形式提供的,而选择具体的加载方式。
- AssetBundle.LoadFromMemoryAsync
- 字节数组
- 从内存里加载
- AssetBundle.LoadFromFile
- 从本地加载
- WWW.LoadFromCacheOrDownload (弃用)
- UnityWebRequest
- 从远程服务器加载
AssetBundle.LoadFromMemoryAsync
此函数采用包含 AssetBundle 数据的字节数组。也可以根据需要传递 CRC 值。如果捆绑包采用的是 LZMA 压缩方式,将在加载时解压缩 AssetBundle。LZ4 压缩包则会以压缩状态加载。
using System.Collections;
using System.IO;
using UnityEngine;
public class LoadFromFileExample : MonoBehaviour {
private void Start() {
StartCoroutine(LoadFromMemoryAsync("Assets/AssetBundles/cubewall.u3d"));
}
IEnumerator LoadFromMemoryAsync(string path) {
// 加载 AB
AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path)); // 字节数组
yield return createRequest;
AssetBundle bundle = createRequest.assetBundle;
// 使用里面的资源
var prefab = bundle.LoadAsset<GameObject>("CubeWall");
Instantiate(prefab);
}
}
AssetBundle.LoadFromFile
- 如果AB未压缩或采用了数据块 (LZ4) 压缩方式,LoadFromFile 将直接从磁盘加载AB包。
- 加载完全压缩的 (LZMA) AB包将首先解压缩捆绑包,然后再将其加载到内存中。
using System.Collections;
using System.IO;
using UnityEngine;
public class LoadFromFileExample : MonoBehaviour {
private void Start() {
StartCoroutine(LoadFromFile("Assets/AssetBundles/cubewall.u3d"));
}
IEnumerator LoadFromFile(string path) {
// 加载 AB
AssetBundleCreateRequest createRequest = AssetBundle.LoadFromFileAsync("Assets/AssetBundles/cubewall.u3d");
yield return createRequest; // 等待加载完成
AssetBundle ab = createRequest.assetBundle;
// 使用里面的资源
var prefab = ab.LoadAsset<GameObject>("CubeWall");
Instantiate(prefab);
}
}
UnityWebRequest
从远程服务器加载 AssetBundle
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
public class LoadFromFileExample : MonoBehaviour {
private void Start() {
StartCoroutine(InstantiateObject());
}
IEnumerator InstantiateObject() {
// 加载 AB
//string uri = @"http://127.0.0.1/AssetBundles/cubewall.u3d";
string uri = "file:///G:/UnityDocuments/AssetBundle/Assets/AssetBundles/cubewall.u3d";
UnityWebRequest request = UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.SendWebRequest();
AssetBundle ab = DownloadHandlerAssetBundle.GetContent(request);
// 使用里面的资源
var prefab = ab.LoadAsset<GameObject>("CubeWall");
Instantiate(prefab);
}
}
从 AB 加载资源
通用代码片段:
// T 是尝试加载的资源类型
T objectFromBundle = bundleObject.LoadAsset<T>(assetName);
同步加载方式:
// 加载单个游戏对象
GameObject gameObject = loadedAssetBundle.LoadAsset<GameObject>(assetName);
// 加载所有资源
Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();
异步加载方式:
// 加载单个游戏对象
AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
yield return request;
var loadedAsset = request.asset;
// 加载所有资源
AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync();
yield return request;
var loadedAssets = request.allAssets;
加载 AssetBundle 清单
当 AB 包存在依赖关系时,如何知道在加载原始包的资源前,需要加载哪些依赖包?
可以通过 Manifest 文件得到某个包的依赖,清单对象可以动态地查找加载依赖项。
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
public class LoadFromFileExample : MonoBehaviour {
private void Start() {
// 加载 ab 包
AssetBundle manifestAB = AssetBundle.LoadFromFile("Assets/AssetBundles/AssetBundles");
// 加载清单文件
AssetBundleManifest manifest = manifestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 加载 ab 的所有依赖项
string[] dependencies = manifest.GetAllDependencies("wall.u3d");
foreach (string dependency in dependencies) {
print(dependency);
AssetBundle.LoadFromFile("Assets/AssetBundles/" + dependency);
}
// 加载资源
AssetBundle ab = AssetBundle.LoadFromFile("Assets/AssetBundles/wall.u3d");
var wallPre = ab.LoadAsset<GameObject>("CubeWall");
// 实例化物体
Instantiate(wallPre);
}
}
AssetBundle 的卸载
了解何时加载和卸载 AssetBundle 非常重要。不正确地卸载 AssetBundle 会导致在内存中复制对象或其他不良情况,例如缺少纹理。
// 卸载 AssetBundle
AssetBundle.Unload(bool);
AssetBundle.Unload(true)
- 卸载所有资源,即使有资源被使用着
- 卸载从 AssetBundle 加载的所有游戏对象(及其依赖项)。这不包括复制的游戏对象(例如实例化的游戏对象),因为它们不再属于 AssetBundle。
- 采用此种方式卸载,从该 AssetBundle 加载的纹理(并且仍然属于它)会从场景中的游戏对象消失,因此 Unity 将它们视为缺少纹理。
AssetBundle.Unload(false)
- 卸载所有没用被使用的资源
- 此种方式可能会在内存中产生大量复制对象
如何选择用哪种方式
假设材质 M 是从 AssetBundle AB 加载的,如下所示。
- 如果调用 AB.Unload(true),活动场景中的任何 M 实例也将被卸载并销毁。
- 如果改作调用 AB.Unload(false),那么将会中断 M 和 AB 当前实例的链接关系。
通常,使用 AssetBundle.Unload(false) 不会带来理想情况。大多数项目应该使用 AssetBundle.Unload(true) 来防止在内存中复制对象。
大多数项目应该使用 AssetBundle.Unload(true) 并采用一种方法来确保对象不会重复。两种常用方法是:
- 在应用程序生命周期中具有明确定义的卸载瞬态 AssetBundle 的时间点,例如在关卡之间或在加载屏幕期间。
- 维护单个对象的引用计数,仅当未使用所有组成对象时才卸载 AssetBundle。这允许应用程序卸载和重新加载单个对象,而无需复制内存。
如果应用程序必须使用 AssetBundle.Unload(false),则只能以两种方式卸载单个对象:
- 在场景和代码中消除对不需要的对象的所有引用。
- 以非附加方式加载场景。
修补 AssetBundle
修补 AssetBundle 很简单,只需要下载新的 AssetBundle 并替换现有的 AssetBundle。
如果使用 UnityWebRequest 来管理应用程序的缓存 AssetBundle,则将不同的版本参数传递给所选 API 将触发新 AssetBundle 的下载。
在修补系统中要解决的更难的问题是检测要替换的 AssetBundle。修补系统需要两个信息列表:
- 当前已下载的 AssetBundle 及其版本控制信息的列表
- 服务器上的 AssetBundle 及其版本控制信息的列表
修补程序应下载服务器端 AssetBundle 列表并比较这些 AssetBundle 列表。应重新下载缺少的 AssetBundle 或已更改版本控制信息的 AssetBundle。
也可以编写一个自定义系统来检测 AssetBundle 的更改。自己编写系统的大多数开发人员会选择对 AssetBundle 文件列表使用行业标准数据格式(例如 JSON)和并使用标准 c sharp 类(例如 MD5)来计算校验和。
常见问题
依赖包重复问题
- 把需要共享的资源打包到一起
- 分割包,这些包不是在同一时间使用的
- 把共享部分打包成一个单独的包
图集重复问题
若不指定 Sprite 的 Packing Tag
,这些 Sprite 将会被自动打包在一个图集里面。
假如 a 包使用了这个图集里的一张图片,那么这个图集就会被打包在 a 包里面,如果 b 包也使用了图集里的一张图片,那么这个图集也会被打包在 b 包里面,这样一来就产生了图集的重复问题。
为了确保精灵图集不重复,请确保将相同精灵图集的所有精灵分配到同一个 AssetBundle。
Android 贴图问题
由于 Android 生态系统中存在严重的设备碎片,因此通常需要将纹理压缩为多种不同的格式。虽然所有 Android 设备都支持 ETC1,但 ETC1 不支持具有 Alpha 通道的纹理。如果应用程序不需要 OpenGL ES 2 支持,解决该问题的最简单方法是使用所有 Android OpenGL ES 3 设备都支持的 ETC2。
解决这个问题的一种方法是使用 Unity 5 的 AssetBundle 变体。(有关其他方案的详细信息,请参阅 Unity 的 Android 优化指南。)
Unity Asset Bundle Browser 工具
下载后,直接将 Editor 文件夹放在自己的项目目录下,此工具使用户能够查看和编辑 Unity 项目的资源包的配置。此工具将在 Window 菜单下创建 AssetBundle Browser 菜单项。
此窗口提供了一个类似资源管理器的界面,用于管理和修改项目中的资源包。