[Unity] 实现AssetBundle资源加载管理器

unity,assetbundle · 浏览次数 : 2

小编点评

**ABManager** 类提供以下方法和属性用于处理 AssetBundle 异步加载: **AB_DIR**:AB包所在目录路径。 **MAIN_AB_NAME**:主包名的扩展名。 **_assetBundles**:一个字典,存储每个 AB 包的加载状态。 **_loadingStatus**:一个字典,存储每个 AB 包的加载进度。 **_loadAssetBundle**方法:异步加载一个 AB 包并将其添加到 AssetBundle 中。 **_loadRes**方法:从 AB 包中加载指定资源并将其传递给回调函数。 **LoadResAsync**方法:使用 IEnumerator 接口异步加载 AB 包并返回一个 T 型结果。 **Unload**方法:释放一个 AB 包并从 AssetBundle 中移除它。 **UnloadAllAssetBundles**方法:清空所有 AB 包和从 AssetBundle 中移除所有资源。 **其他方法:** * **_checkStatus**:检查 AB 包的加载状态。 * **_setStatus**:设置 AB 包的加载状态。 * **_loadAssetBundleAsync**:异步加载 AB 包并返回 IEnumerator。 * **_loadResAsync**:使用 IEnumerator 接口异步加载 AB 包并返回结果。 * **LoadResAsync**:使用 UnityAction 回调器异步加载 AB 包并处理结果。 * **Unload**:释放一个 AB 包并从 AssetBundle 中移除它。 * **UnloadAllAssetBundles**:清空所有 AB 包和从 AssetBundle 中移除所有资源。

正文

实现Unity AssetBundle资源加载管理器

AssetBundle是实现资源热更新的重要功能,但Unity为其提供的API却十分基(jian)础(lou)。像是自动加载依赖包、重复加载缓存、解决同步/异步加载冲突,等基础功能都必须由使用者自行实现。

因此,本篇博客将会介绍如何实现一个AssetBundle管理器以解决以上问题。

1 成员定义与初始化

作为典型的"Manager"类,我们显然要让其成为一个单例对象,并且由于后续异步加载会用到协程函数,因此还需要继承MonoBehaviour。所以,这里用到了我在Unity单例基类的实现方式中提到的Mono单例基类SingletonMono<>

// Mono单例基类
public abstract class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance;

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                // 在场景中查找是否已存在该类型的实例
                _instance = FindObjectOfType<T>();

                // 如果场景中不存在该类型的实例,则创建一个新的GameObject并添加该组件
                if (_instance == null)
                {
                    GameObject singletonObject = new GameObject(typeof(T).Name + "(Singleton)");
                    DontDestroyOnLoad(singletonObject); // 保留在场景切换时不被销毁
                    _instance = singletonObject.AddComponent<T>();
                }
            }
            return _instance;
        }
    }
}

在加载AB包时,我们一般只要求外部传入包名,但AssetBundle.LoadFromFile是需要完整路径的,因此我们可以根据自己打包时的具体位置来修改AB_DIR。由于我在打包时勾选了Copy to StreamingAssets,因此这里就用Application.streamingAssetsPath + '/'作为AB包的根目录。

private static readonly string AB_DIR = ... + '/';    // AB包所在目录

AB包之间的依赖信息都存储在主包的Manifest之中,所以我们需要先设置好主包的名字。这里的MAIN_AB_NAME的值也是根据你在打包时的参数来修改的,比如我打包的Output Path参数是AssetBundles/PC,那么此时主包名就是PC

private static readonly string MAIN_AB_NAME =   // 主包名
#if UNITY_IOS
        "iOS";
#elif UNITY_ANDROID
        "Android";
#else
        "PC";
#endif

接下来就需要在Awake函数中进行初始化,唯一要做的就是读取主包的Manifest

public class ABManager : SingletonMono<ABManager>
{
    // ......

    private AssetBundleManifest _mainManifest;

    private void Awake()
    {
        // 加载主包的manifest
        AssetBundle mainAssetBundle = AssetBundle.LoadFromFile(AB_DIR + MAIN_AB_NAME);
        _mainManifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        mainAssetBundle.Unload(false); // 加载完manifest之后就可以释放主包
    }

    // ......
}

同一个AB包在被多次加载时会报错,所以我们需要声明一个字典来存储已经加载的AB包。

private readonly Dictionary<string, AssetBundle> _assetBundles = new();

此外我们还要注意同步/异步冲突异步/异步冲突

同步/异步冲突是指,在某个AB包异步加载的过程中,用户又对同一个AB包发起了同步加载的请求,如果我们直接进行同步加载,就会出现“同一个AB包在被多次加载”的错误。

异步/异步冲突则是,在某个AB包异步加载的过程中,用户又对同一个AB包发起了异步加载的请求同样会重复加载的错误,因此我们就需要让后来的异步请求进行暂停等待,直到该包在先来的异步请求中加载完成。

为此我们需要定义一组加载状态,用于解决上述冲突,并且使用字典来存储AB包当前的加载状态

enum ABStatus
{
    Completed,  // 本包和依赖包都加载完毕
    Loading,    // 正在加载
    NotLoaded   // 未被加载
}
private readonly Dictionary<string, ABStatus> _loadingStatus = new();

综上所述,我们的成员定义与初始化如下:

public class ABManager : SingletonMono<ABManager>
{
    private static readonly string AB_DIR = Application.streamingAssetsPath + '/';    // AB包所在目录
    private static readonly string MAIN_AB_NAME =   // 主包名
#if UNITY_IOS
        "iOS";
#elif UNITY_ANDROID
        "Android";
#else
        "PC";
#endif

    private AssetBundleManifest _mainManifest;
    private readonly Dictionary<string, AssetBundle> _assetBundles = new();
    private readonly Dictionary<string, ABStatus> _loadingStatus = new();

    private void Awake()
    {
        // 加载主包的manifest
        AssetBundle mainAssetBundle = AssetBundle.LoadFromFile(AB_DIR + MAIN_AB_NAME);
        _mainManifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        mainAssetBundle.Unload(false); // 加载完manifest之后就可以释放主包
    }

    // ......
}

2 卸载AB包

接着来我们来实现最简单的AB包卸载功能。

卸载单个AB包只需要根据传入的包名,调用对应AB包的Unload方法,然后再从_assetBundles_loadingStatus中将该包名移除。

public void Unload(string abName, bool unloadAllLoadedObjects = false)
{
    if (!_assetBundles.ContainsKey(abName) || _assetBundles[abName] == null)
    {
        return;
    }

    _assetBundles[abName].Unload(unloadAllLoadedObjects);
    _assetBundles.Remove(abName);
    _loadingStatus.Remove(abName);
}

卸载所有AB包则是直接清空_assetBundles_loadingStatus的记录,然后调用Unity提供的AssetBundle.UnloadAllAssetBundles卸载所有AB包即可。

public void UnloadAllAssetBundles(bool unloadAllLoadedObjects = false)
{
    _assetBundles.Clear();
    _loadingStatus.Clear();
    AssetBundle.UnloadAllAssetBundles(unloadAllLoadedObjects);
}

3 同步加载

为了增加代码的可读性,让我们先定义以下两个函数,用于检查和设置AB包的状态。

private ABStatus _checkStatus(string abName)
{
    return _loadingStatus.TryGetValue(abName, out ABStatus value)
                ? value : ABStatus.NotLoaded;
}

private void _setStatus(string abName, ABStatus status)
{
    _loadingStatus[abName] = status;
}

3.1 同步加载AB包

在加载资源之前肯定需要先加载AB包。将传入的包名作为加载队列的初值,之后遍历加载队列中的包名进行加载。

同步加载完一个AB包后,再将其所有的依赖包都加入到加载队列中,进行下一轮的加载。

由于同步加载的特性,可以保证在本次调用中完成所有AB包及其依赖的加载,因此加载状态可以直接设置为Completed

为了解决同步/异步冲突,对于正在异步中加载的包,我们可以直接调用Unload进行卸载,这样一来就可以打断正在进行的异步加载

private void _loadAssetBundle(string abName)
{
    Queue<string> loadQueue = new();
    loadQueue.Enqueue(abName);

    for (; loadQueue.Count > 0; loadQueue.Dequeue())
    {
        string name = loadQueue.Peek();

        // 跳过已完成的包
        if (_checkStatus(name) == ABStatus.Completed)
        {
            continue;
        }
        // 打断正在异步加载的包
        if (_checkStatus(name) == ABStatus.Loading)
        {
            Unload(name);
        }

        // 同步方式加载AB包
        _assetBundles[name] = AssetBundle.LoadFromFile(AB_DIR + name);
        if (_assetBundles[name] == null)
        {
            throw new ArgumentException($"AssetBundle '{name}' 加载失败");
        }
        _setStatus(name, ABStatus.Completed);

        // 添加依赖包到待加载列表中
        foreach (var depend in _mainManifest.GetAllDependencies(name))
        {
            loadQueue.Enqueue(depend);
        }
    }
}

3.2 同步加载资源

AB包加载完成之后,就可以直接从记录中获取对应的AssetBundle对象来加载资源了。

public T LoadRes<T>(string abName, string resName) where T : UnityEngine.Object
{
    if (_checkStatus(abName) != ABStatus.Completed)
    {
        _loadAssetBundle(abName);
    }
    T res = _assetBundles[abName].LoadAsset<T>(resName);
    if (res == null)
    {
        throw new ArgumentException($"无法从AssetBundle '{abName}' 中获取资源 '{resName}'。");
    }
    return res;
}

注意
这里不要缩写成 return res ?? throw new ArgumentException(...)的形式
因为这里的泛型T被约束为UnityEngine.Object,而Unity Object使用null合并运算符会导致意外情况
有的编辑器(比如VSCode插件)可能没有正确判断约束的上下文
没识别出T是UnityEngine.Object,从而提示使用??进行缩写,请忽略这种提示
详细情况可以参考Unity官方的说明:
https://blog.unity.com/engine-platform/custom-operator-should-we-keep-it

4 异步加载

4.1 异步加载AB包

AB包的异步加载和同步加载的策略有很大的不同。

当我们说某个AB包加载完成时,不单是指它的本体加载完毕,还需要它的依赖包也全部加载完成,而依赖包又需要“依赖包的依赖包”加载完成。

由于同步加载能够保证所有的AB包都能在本次调用中加载完毕,因此我们并不关心AB包的先后顺序。

但异步加载是分段的,所以我们必须保证其本体和所有依赖包都加载完成后,才将状态设为Completed,而对于依赖包来说也是如此。一般我们会用递归来处理这种情况,但”协程递归“这种方案听名字就该Pass掉(bushi),这里完全可以用来模拟这一过程。

我们先声明一个存储二元组的栈,用于表示包名和标记位。

Stack<(string name, bool needAddDepends)> loadStack = new();

对于入栈的AB包,我们先假设它还有依赖包需要加载,也就是needAddDepends默认为true。接着每次循环过程中,我们都查看栈顶的信息,如果标记为true,则设为false,然后将其所有的依赖包入栈(同样假设这些依赖包也有依赖要处理),并且需要防止重复添加包(环形依赖)导致死循环。这样就能保证在加载某个AB包前先完成其依赖包的加载。

另外,我们还需要处理异步/异步冲突:当某个AB包处于Loading状态时,表示有另一个协程在异步加载该AB包,这时就需要暂停等待直到该包被加载完毕。

private IEnumerator _loadAssetBundleAsync(string abName)
{
    HashSet<string> visitedBundles = new() { abName };
    Stack<(string name, bool needAddDepends)> loadStack = new();
    loadStack.Push((abName, true));

    while (loadStack.Count > 0)
    {
        var (name, needAddDepends) = loadStack.Peek();

        // 跳过已完成的包
        if (_checkStatus(name) == ABStatus.Completed)
        {
            loadStack.Pop();
            continue;
        }
        // 暂停等待正在加载的包
        if (_checkStatus(name) == ABStatus.Loading)
        {
            yield return null;
            continue;
        }
        // 先处理依赖包
        if (needAddDepends)
        {
            loadStack.Pop();
            loadStack.Push((name, false));

            foreach (var depend in _mainManifest.GetAllDependencies(name))
            {
                if (visitedBundles.Add(depend))
                {
                    loadStack.Push((depend, true));
                }
            }

            continue;
        }

        // 异步加载AB包
        AssetBundleCreateRequest abCreateRequest = AssetBundle.LoadFromFileAsync(AB_DIR + name);
        _assetBundles[name] = abCreateRequest.assetBundle;
        _setStatus(name, ABStatus.Loading);
        if (_assetBundles[name] == null)
        {
            throw new ArgumentException($"AssetBundle '{name}' 加载失败");
        }
        yield return abCreateRequest;
        // 加载完成
        _setStatus(name, ABStatus.Completed);
    }
}

4.2 异步加载资源

处理完AB包的加载之后就只需要发起异步资源请求并做错误处理即可。

private IEnumerator _loadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : UnityEngine.Object
{
    // 等待异步加载AB包
    if (_checkStatus(abName) != ABStatus.Completed)
    {
        yield return StartCoroutine(_loadAssetBundleAsync(abName));
    }
    // 异步加载资源
    AssetBundleRequest abRequest = _assetBundles[abName].LoadAssetAsync<T>(resName);
    yield return abRequest;

    T res = abRequest.asset as T;
    // 错误处理:资源不存在
    if (res == null)
    {
        throw new ArgumentException($"无法从AssetBundle '{abName}' 中获取资源 '{resName}'。");
    }
    // 回调
    callBack(res);
}

public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : UnityEngine.Object
{
    StartCoroutine(_loadResAsync<T>(abName, resName, callBack));
}

5 完整代码

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;

enum ABStatus
{
    Completed,  // 本包和依赖包都加载完毕
    Loading,    // 正在加载
    NotLoaded   // 未被加载
}

public class ABManager : SingletonMono<ABManager>
{
    private static readonly string AB_DIR = Application.streamingAssetsPath + '/';    // AB包所在目录
    private static readonly string MAIN_AB_NAME =   // 主包名
#if UNITY_IOS
        "iOS";
#elif UNITY_ANDROID
        "Android";
#else
        "PC";
#endif

    private AssetBundleManifest _mainManifest;
    private readonly Dictionary<string, AssetBundle> _assetBundles = new();
    private readonly Dictionary<string, ABStatus> _loadingStatus = new();

    private void Awake()
    {
        // 加载主包的manifest
        AssetBundle mainAssetBundle = AssetBundle.LoadFromFile(AB_DIR + MAIN_AB_NAME);
        _mainManifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        mainAssetBundle.Unload(false); // 加载完manifest之后就可以释放主包
    }

    private ABStatus _checkStatus(string abName)
    {
        return _loadingStatus.TryGetValue(abName, out ABStatus value)
                    ? value : ABStatus.NotLoaded;
    }

    private void _setStatus(string abName, ABStatus status)
    {
        _loadingStatus[abName] = status;
    }

    private void _loadAssetBundle(string abName)
    {
        Queue<string> loadQueue = new();
        loadQueue.Enqueue(abName);

        for (; loadQueue.Count > 0; loadQueue.Dequeue())
        {
            string name = loadQueue.Peek();

            // 跳过已完成的包
            if (_checkStatus(name) == ABStatus.Completed)
            {
                continue;
            }
            // 打断正在异步加载的包
            if (_checkStatus(name) == ABStatus.Loading)
            {
                Unload(name);
            }

            // 同步方式加载AB包
            _assetBundles[name] = AssetBundle.LoadFromFile(AB_DIR + name);
            if (_assetBundles[name] == null)
            {
                throw new ArgumentException($"AssetBundle '{name}' 加载失败");
            }
            _setStatus(name, ABStatus.Completed);

            // 添加依赖包到待加载列表中
            foreach (var depend in _mainManifest.GetAllDependencies(name))
            {
                loadQueue.Enqueue(depend);
            }
        }
    }

    public T LoadRes<T>(string abName, string resName) where T : Object
    {
        if (_checkStatus(abName) != ABStatus.Completed)
        {
            _loadAssetBundle(abName);
        }
        T res = _assetBundles[abName].LoadAsset<T>(resName);
        if (res == null)
        {
            throw new ArgumentException($"无法从AssetBundle '{abName}' 中获取资源 '{resName}'。");
        }
        return res;
    }

    private IEnumerator _loadAssetBundleAsync(string abName)
    {
        HashSet<string> visitedBundles = new() { abName };
        Stack<(string name, bool needAddDepends)> loadStack = new();
        loadStack.Push((abName, true));

        while (loadStack.Count > 0)
        {
            var (name, needAddDepends) = loadStack.Peek();

            // 跳过已完成的包
            if (_checkStatus(name) == ABStatus.Completed)
            {
                loadStack.Pop();
                continue;
            }
            // 暂停等待正在加载的包
            if (_checkStatus(name) == ABStatus.Loading)
            {
                yield return null;
                continue;
            }
            // 先处理依赖包
            if (needAddDepends)
            {
                loadStack.Pop();
                loadStack.Push((name, false));

                foreach (var depend in _mainManifest.GetAllDependencies(name))
                {
                    if (visitedBundles.Add(depend))
                    {
                        loadStack.Push((depend, true));
                    }
                }

                continue;
            }

            // 异步加载AB包
            AssetBundleCreateRequest abCreateRequest = AssetBundle.LoadFromFileAsync(AB_DIR + name);
            _assetBundles[name] = abCreateRequest.assetBundle;
            _setStatus(name, ABStatus.Loading);
            if (_assetBundles[name] == null)
            {
                throw new ArgumentException($"AssetBundle '{name}' 加载失败");
            }
            yield return abCreateRequest;
            // 加载完成
            _setStatus(name, ABStatus.Completed);
        }
    }

    private IEnumerator _loadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
    {
        // 等待异步加载AB包
        if (_checkStatus(abName) != ABStatus.Completed)
        {
            yield return StartCoroutine(_loadAssetBundleAsync(abName));
        }
        // 异步加载资源
        AssetBundleRequest abRequest = _assetBundles[abName].LoadAssetAsync<T>(resName);
        yield return abRequest;

        T res = abRequest.asset as T;
        // 错误处理:资源不存在
        if (res == null)
        {
            throw new ArgumentException($"无法从AssetBundle '{abName}' 中获取资源 '{resName}'。");
        }
        // 回调
        callBack(res);
    }

    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
    {
        StartCoroutine(_loadResAsync<T>(abName, resName, callBack));
    }

    public void Unload(string abName, bool unloadAllLoadedObjects = false)
    {
        if (!_assetBundles.ContainsKey(abName) || _assetBundles[abName] == null)
        {
            return;
        }

        _assetBundles[abName].Unload(unloadAllLoadedObjects);
        _assetBundles.Remove(abName);
        _loadingStatus.Remove(abName);
    }

    public void UnloadAllAssetBundles(bool unloadAllLoadedObjects = false)
    {
        _assetBundles.Clear();
        _loadingStatus.Clear();
        AssetBundle.UnloadAllAssetBundles(unloadAllLoadedObjects);
    }
}

参考资料

解决 Unity3D AssetBundle 异步加载与同步加载冲突问题

Custom == operator, should we keep it?

C#语法糖 (?) null空合并运算符对UnityEngine.Object类型不起作用


本文发布于2024年5月23日

最后编辑于2024年5月23日

与[Unity] 实现AssetBundle资源加载管理器相似的内容:

[Unity] 实现AssetBundle资源加载管理器

实现Unity AssetBundle资源加载管理器 AssetBundle是实现资源热更新的重要功能,但Unity为其提供的API却十分基(jian)础(lou)。像是自动加载依赖包、重复加载缓存、解决同步/异步加载冲突,等基础功能都必须由使用者自行实现。 因此,本篇博客将会介绍如何实现一个Ass

扩展实现Unity协程的完整栈跟踪

现如今Unity中的协程(Coroutine)方案已显得老旧,Unitask等异步方案可以直接解决如异常捕获等各类问题, 并且Unity官方也在开发一套异步方案,但现阶段还是需要在协程这个方案上继续琢磨。 Unity协程中无法输出完整的栈跟踪,因为协程编译后会转换为IL编码的状态机,中间存在栈回到堆

.NET周刊【5月第2期 2024-05-12】

国内文章 C#在工业数字孪生中的开发路线实践 https://mp.weixin.qq.com/s/b_Pjt2oii0Xa_sZp_9wYWg 这篇文章探讨了C#在工业数字孪生技术中的应用,介绍了三种基于C#的数字孪生系统实现方案: WPF + Unity:结合WPF技术和Unity引擎,实现客户

[Unity] Dreamteck Splines实现沿路径移动功能

Dreamteck Splines实现沿路径移动功能 最近有一个“让物体沿固定路径移动”的需求,因此接触到了Dreamteck Splines插件。 Dreamteck Splines可以很方便地绘制各种插值曲线,但在实现物体移动的时候却遇到了很多坑,因此在这里记录一下。 1. 绘制路径线 首先,让

Unity 利用Cache实现边下边玩

现在手机游戏的常规更新方案都是在启动时下载所有资源更新,游戏质量高的、用户粘性大的有底气,先安装2个G,启动再更新2个G,文件小了玩家还觉得品质不行不想玩。 最近在做微信、抖音小游戏,使用他们提供的资源缓存方案,现在要转成Android APP, 也想用这种边下边玩的机制把首包做小。 其实很简单,直

关于Unity 如何与Blazor Server结合

关于Unity 如何与Blazor Server结合 一、介绍 最近工作中有`Unity`与`Blazor Server`结合的需求,在网上找了一圈,发现这方面的资料比较少,特此写下这篇记录一下自己的实现过程,希望可以帮到有需要的朋友。(下方多图预警) OS Version : windows 11

集成Unity3D到iOS应用程序中

如果想让原生平台(例如 Java/Android、Objective C/iOS 或 Windows Win32/UWP)包含 Unity 功能,可以通过Unity 生成UnityFramework静态库包含到项目中进行实现。 Unity 从2019.3 开始支持将 Unity 运行时组件集成到原生

在原生APP中集成Unity容器

随着技术的发展,越来越多的APP期望拥有3D,AR的能力。要达到这个目标可以选择使用原生开发,也可以使用Unity成熟的3D开发技术链,通过嵌入的方式将Unity容器嵌入到APP中。这里介绍的是通过嵌入Unity容器的方式来实现APP的3D,AR能力的。 Unity集成到iOS应用的本质是将Unit

基于助听器开发的一种高效的语音增强神经网络

现代语音增强算法利用大量递归神经网络(RNNs)实现了显著的噪声抑制。然而,大型RNN限制了助听器硬件(hearing aid hardware,HW)的实际部署,这些硬件是电池供电的,运行在资源受限的微控制器单元(microcontroller units,MCU)上,内存和计算能力有限。在这项工

[转帖]Redis配置文件介绍

https://www.cnblogs.com/fanqisoft/p/10422381.html Redis在源码包中存放了一个Redis配置实例文件,文件中对各个配置点进行了简单的介绍,我也通过这个文件来对Redis的一些配置进行一些简单介绍。 一.UNITS(单位)【了解】 1.Redis服务