Bootstrap

【Unity学习笔记】AssetBundle


什么是AB包?

AssetBundle是Unity提供的一种用于存储资源的压缩集合,它可以存储任何一种Unity可以识别的资源,如模型,纹理图,音频,场景等资源。也可以加载开发者自定义的二进制文件。

为什么使用AB包?

Resources打包时会将所有的资源统统打包到Resources中,这意味着即使你只想要其中的部分资源,也需要对所有资源全部打包带走;并且由于它是只读的,所以我们不能随意地修改包的内容。

相对的使用AB包能够分别将不同的资源打包到独立的包体内,AB包存储路径可自定义,压缩方式可自定义,后期也可以对已经打包的内容进行动态更新。

AB包可以节约包体资源大小。一方面在压缩时AB包允许更好的压缩方式,以及选取的压缩资源也可以随意选择。另一方面,例如在游戏客户端的时候,由于AB包是可修改的,因此可以减少初始的安装大小,在安装游戏的时候我们可以设置一个可供游戏运行的较小的AB包,当玩家更新时则可以把其余的AB包内容下载完。

AB包还可以方便我们进行热更新,例如玩王者的时候,我们并没有下载更新,而游戏界面却更新了。这可以通过游戏内部对已经下载的AB包资源进行热更新而实现。而在游戏需要更新下载的时候,通常是先让客户端与服务器通信后,获取资源服务器的地址,然后和资源服务器的AB包进行对比来判断需要更新的资源。


如何导出AB包

在安装了AB包服务后如何导出AB包?

1.选中物体,在Inspector界面下方输入(选择)绑定的包体名称
2.在AssetBundles面板查看已经打的包
3.选择导出选项

此处介绍一下导出选项

属性选项
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 HashassetBundle 名称附加哈希(哈希寻址,文件校验)
Strict Mode如果在此期间报告任何错误,则构建无法成功
Dry Run Build进行干运行构建。此选项允许您针对 AssetBundle 进行干运行构建,但不实际构建它们。启用此选项后,BuildPipeline.BuildAssetBundles 仍将返回 AssetBundleManifest 对象,其中包含有效的 AssetBundle 依赖关系和哈希。

此外在unity中,C#是不能打包成AB包的,因此我们需要学习Lua(哈哈,太搞笑了)。打包成AB包时物体上的C#脚本其实只包含了对应FileID的映射。

4.在Inspect中打开对应资源可以检查导出完毕的AB包中的信息

此外,被打包的物体所使用的那些资源,例如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包管理器,就可以避免项目中混乱的包体关系,推荐指数五颗星。

;