什么是AB包?
AssetBundle是Unity提供的一种用于存储资源的压缩集合,它可以存储任何一种Unity可以识别的资源,如模型,纹理图,音频,场景等资源。也可以加载开发者自定义的二进制文件。
为什么使用AB包?
Resources打包时会将所有的资源统统打包到Resources中,这意味着即使你只想要其中的部分资源,也需要对所有资源全部打包带走;并且由于它是只读的,所以我们不能随意地修改包的内容。
相对的使用AB包能够分别将不同的资源打包到独立的包体内,AB包存储路径可自定义,压缩方式可自定义,后期也可以对已经打包的内容进行动态更新。
AB包可以节约包体资源大小。一方面在压缩时AB包允许更好的压缩方式,以及选取的压缩资源也可以随意选择。另一方面,例如在游戏客户端的时候,由于AB包是可修改的,因此可以减少初始的安装大小,在安装游戏的时候我们可以设置一个可供游戏运行的较小的AB包,当玩家更新时则可以把其余的AB包内容下载完。
AB包还可以方便我们进行热更新,例如玩王者的时候,我们并没有下载更新,而游戏界面却更新了。这可以通过游戏内部对已经下载的AB包资源进行热更新而实现。而在游戏需要更新下载的时候,通常是先让客户端与服务器通信后,获取资源服务器的地址,然后和资源服务器的AB包进行对比来判断需要更新的资源。
如何导出AB包
在安装了AB包服务后如何导出AB包?
此处介绍一下导出选项
属性 | 选项 |
---|---|
Build Target | 选项对应导出的平台 |
Output | 导出路径,AB包根路径AssetBundles与Asset同级 |
Clear Folders | 清空导出文件夹 |
Copy to StreamingAssets | 导出完毕后,导出的AB包复制到Assets文件夹中的StreamingAssets中 |
一些比较重要的参数
属性 | 选项 |
---|---|
Compression | 压缩模式:No Compression无压缩,不用解包,优点是读取快,但是文件大 |
LZMA标准压缩,压缩率高,但解包麻烦 | |
LZ4区块压缩,当使用包内的资源时,只会解压对应的chunk,内存占用率低, 强烈推荐 | |
Exclude Type Information | 不包含资源类型信息 |
Force Rebuild | 强制重构包内所有内容 |
Ignore Type Tree Changes | 在执行增量构建检查时忽略类型树更改 |
Append Hash | assetBundle 名称附加哈希(哈希寻址,文件校验) |
Strict Mode | 如果在此期间报告任何错误,则构建无法成功 |
Dry Run Build | 进行干运行构建。此选项允许您针对 AssetBundle 进行干运行构建,但不实际构建它们。启用此选项后,BuildPipeline.BuildAssetBundles 仍将返回 AssetBundleManifest 对象,其中包含有效的 AssetBundle 依赖关系和哈希。 |
此外在unity中,C#是不能打包成AB包的,因此我们需要学习Lua(哈哈,太搞笑了)。打包成AB包时物体上的C#脚本其实只包含了对应FileID的映射。
此外,被打包的物体所使用的那些资源,例如shader,sprite等,即使没有主动将它们打包,它们也会被自动地打包(并且默认与使用它的物体在同一个包体中)。此时我们可以在AssetLabel里选择它属于哪个包。但是要注意,如果物体所依赖的资源被打到其他的包中,那么单独加载物体往往会造成其他依赖资源的丢失,因此在加载这个物体的时候我们还需要同时加载它的依赖包。
AB包导出文件
打包完成后,每个包会对应生成一个包和一个对应的manifest清单,此外还有一个与文件夹同名的包,称为主包,它储存了包与包之间的依赖关系的信息。所有的包没有后缀名,是二进制文件。Manifest包含了资源中的信息。
我们往往会勾选StreamingAssets,这是为了方便在代码中进行调用,通常使用:
Application.streamingAssetsPath + "/" +"包名"
来读取AB包的路径
如何使用AB包
AB包的加载
AB包的加载分为两步,第一步先加载AB包,第二步加载包内对应的资源
同步加载
最简单的方法就是同步的方式加载AB包资源,所有代码全部依次执行
首先加载AB包
AssetBundle AB = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "包名");
然后加载资源,资源加载有两种方式,一种通过泛型加载,另一种直接加载并指定类型。
// T对应加载的资源的类型
T obj = AB.LoadAsset<T>("包内资源名");
// T对应加载的资源的类型
T obj = AB.LoadAsset("包内资源名", typeof(T)) as T;
从实用性上来说第一种加载方式好,但是从实践上来说第二种更好,因为Lua不支持泛型。
在这种情况下
异步加载
异步加载是我们推荐的方式,因为在Unity是单线程的,我们不希望同步加载时由于资源数量多导致的主线程阻塞,这会造成游戏卡死。说到异步加载,那第一个肯定想到的是协程,除了用协程加载,AB包也提供了一些异步加载方法。
IEnumerator LoadABres(string ABName, string resName)
{
AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + ABName);
yield return abcr;
AssetBundleRequest abq = abcr.assetBundle.LoadAssetAsync(resName, typeof(Sprite));
yield return abq;
// 以加载Image组件的Sprite为例
Sprite.sprite = abq.asset as Sprite;
}
AB包的卸载
AssetBundle.UnloadAllAssetBundles(true);
AssetBundle.UnloadAllAssetBundles(false);
UnloadAllAssetBundles方法可以一键卸载全部的AB包,其中包含两种模式,true模式下会直接卸载场景中的AB包,这会导致场景资源丢失。所以推荐使用false,false时会卸载全部AB包并且场景资源不丢失。
同样的我们可以用Unload方法卸载指定AB包,同样的true和false。
AB.Unload(true);
AB.Unload(false);
依赖加载
如果一个模型依赖一个资源,我们可以将两个包都加载。但是如果一个模型依赖好多资源,难道我们要手动加载这么多包?更糟糕的是如果你不知道依赖包是哪个该怎么办?
因此我们需要主包来获取依赖信息
// 加载主包
AssetBundle abMain = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "主包包名");
// 获取主包清单
AssetBundleManifest abManifest = abMain.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 从清单中获取依赖信息并保存在一个字符串数组中
string[] strs = abManifest.GetAllDependencies("需要加载的包名");
// for循环或者foreach都可以循环加载依赖包
foreach (string str in strs)
{
AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + str);
}
有个问题,现在我们的确加载了使用包体的资源。但是如果在另一个包中,它的依赖包和这个包的依赖包是相同的,那么如果我们加载了另一个包,就会报错!因为同一个依赖包被加载了两次。
为了避免这种问题,我们需要一个管理器来统一管理资源包的加载卸载!
AB包资源管理器
首先我们思考,一个资源管理器应当有哪些特点:
- 全局唯一,否则其他类的调用依旧会导致重复加载(单例模式)
- 字典存储,列出一个包体使用清单,并且键值对能避免重复写入
- 异步加载,保证主线程安全
那么一个单例模式的资源管理器将是最佳的选择。
主要过程分为四步:
- 写一个函数实现读取主包获取依赖关系
- 定义一个字典以键值对形式存储包名和AB包资源之间的映射,字典可以避免重复读取同一个包
- 重写Unity的读取AB包方法,使之先读取主包获取所读AB包的依赖关系,随后读取该AB包和所有依赖包
- 定义卸载方法,清空字典
- (以及其他的自定义可拓展的方法)
这里仅给出一小段代码:
定义一个DontDestroyOnLoad的接受泛型的单例基类
/// <summary>
/// 该单例继承于Mono,并且不会随着场景的切换而销毁
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class DDOLSingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
if (_instance == null)
{
GameObject obj = new GameObject();
obj.name = typeof(T).Name;
DontDestroyOnLoad(obj);
_instance = obj.AddComponent<T>();
}
}
return _instance;
}
}
}
定义一个ABManager,它继承了上述基类,并在其内定义管理方法
/// <summary>
/// 加载AB包
/// </summary>
/// <param name="abName"></param>
public void LoadAB(string abName)
{
// 获取主包
if (mainAB == null)
{
mainAB = AssetBundle.LoadFromFile(PathUrl + MainABName);
manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
}
AssetBundle ab = null;
// 获取所用资源包依赖信息
string[] relys = manifest.GetAllDependencies(abName);
foreach (string rely in relys)
{
// 判断依赖包是否已加载
if (!abDic.ContainsKey(rely))
{
ab = AssetBundle.LoadFromFile(PathUrl + rely);
abDic.Add(rely, ab);
}
}
// 加载资源包
if (!abDic.ContainsKey(abName))
{
ab = AssetBundle.LoadFromFile(PathUrl + abName);
abDic.Add(abName, ab);
}
}
/// <summary>
/// 异步加载资源,GameObject类型可直接帮助实例化
/// </summary>
/// <param name="abName">加载的资源包</param>
/// <param name="resName">加载的资源名称</param>
/// <param name="callBack"></param>
public void LoadResAsync(string abName, string resName, UnityAction<Object> callBack)
{
StartCoroutine(LoadResAsyncCoroutine(abName, resName, callBack));
}
private IEnumerator LoadResAsyncCoroutine(string abName, string resName, UnityAction<Object> callBack)
{
LoadAB(abName);
// 如果是GameObject,帮助实例化
AssetBundleRequest abr = abDic[abName].LoadAssetAsync(resName);
yield return abr;
if (abr.asset == null)
{
Debug.LogError("异步加载失败!读取资源为空!");
}
if (abr.asset is GameObject)
callBack(Instantiate(abr.asset));
else
callBack(abr.asset);
}
其他代码也大同小异,无非就是重写官方的三种同步加载和异步加载的方法,使之先读取依赖包,顺便能帮助GameObject进行实例化。
具体代码可以查看【唐老狮】Unity热更新之AssetBundle
使用AB包管理器,就可以避免项目中混乱的包体关系,推荐指数五颗星。