实现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资源加载管理器的更多相关文章

  1. 详谈 Unity3D AssetBundle 资源加载,结合实际项目开发实例

    第一次搞资源更新方面,这里只说更新,加载,AssetBundle资源加载,谈谈自己的理解,以及自己在项目中遇到的那些神坑,现在回想一下,真的是自己跪着过来的,说多了,都是泪. 我这边是安卓AssetB ...

  2. Unity 4.x 资源加载

    using UnityEngine; using System.Collections; using System.IO; public class LoadResource : MonoBehavi ...

  3. imagepool前端图片加载管理器(JavaScript图片连接池)

    前言 imagepool是一款管理图片加载的JS工具,通过imagepool可以控制图片并发加载个数. 对于图片加载,最原始的方式就是直接写个img标签,比如:<img src="图片 ...

  4. Unity -- AssetBundle(本地资源加载和加载依赖关系)

    1.本地资源加载 1).建立Editor文件夹 2).建立StreamingAssets文件夹和其Windows的子文件夹 将下方第一个脚本放入Editor 里面 脚本一  资源打包AssetBund ...

  5. 细谈unity资源加载和卸载

    转载请标明出处:http://www.cnblogs.com/zblade/ 一.概要 在了解unity的资源管理方式之后,接下来细谈一下Unity的资源是如何从磁盘中加载到运行时的内存中,以及又是如 ...

  6. AssetBundle使用心得【资源加载】

    0.资源加载方式 静态资源 Asset下所有资源称为静态资源 Resources资源 Resources目录下,通过实例化得到的资源 AssetBundle资源 又称为增量更新资源 1.什么是Asse ...

  7. libgdx学习记录16——资源加载器AssetManager

    AssetManager用于对游戏中的资源进行加载.当游戏中资源(图片.背景音乐等)较大时,加载时会需要较长时间,可能会阻塞渲染线程,使用AssetManager可以解决此类问题. 主要优点: 1. ...

  8. Android之Android apk动态加载机制的研究(二):资源加载和activity生命周期管理

    转载请注明出处:http://blog.csdn.net/singwhatiwanna/article/details/23387079 (来自singwhatiwanna的csdn博客) 前言 为了 ...

  9. 手撸Spring框架,设计与实现资源加载器,从Spring.xml解析和注册Bean对象

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 你写的代码,能接的住产品加需求吗? 接,是能接的,接几次也行,哪怕就一个类一片的 i ...

  10. Spring资源加载器抽象和缺省实现 -- ResourceLoader + DefaultResourceLoader(摘)

    概述 对于每一个底层资源,比如文件系统中的一个文件,classpath上的一个文件,或者一个以URL形式表示的网络资源,Spring 统一使用 Resource 接口进行了建模抽象,相应地,对于这些资 ...

随机推荐

  1. C语言 05 变量与常量

    变量 变量就像在数学中学习的 x,y 一样,可以直接声明一个变量,并利用这些变量进行基本的运算,声明变量的格式为: 数据类型 变量名称 = 初始值;(其中初始值可以不用在定义变量时设定) = 是赋值操 ...

  2. win10上鼠标右键怎么进入cmd

    背景: 在win7上有个很好的功能,在文件夹空白区域,按住 " ctrl + 鼠标右键 " 可以直接打开 cmd 窗口 但是在win10上同样的操作方法,打开的窗口却是 power ...

  3. 嘉楠k210 kd233官方demo板gpio点灯实验

    使用maixpy  micropython开发 import utime from Maix import GPIO from board import board_info from fpioa_m ...

  4. 容器基础-- namespace,Cgroup 和 UnionFS

    Namespace 什么是 Namespace ? 这里的 "namespace" 指的是 Linux namespace 技术,它是 Linux 内核实现的一种隔离方案.简而言之 ...

  5. Java实现控制台购书系统

    "感谢您阅读本篇博客!如果您觉得本文对您有所帮助或启发,请不吝点赞和分享给更多的朋友.您的支持是我持续创作的动力,也欢迎留言交流,让我们一起探讨技术,共同成长!谢谢!" 代码 im ...

  6. linux系统关闭指定端口

    linux系统关闭指定端口 关闭指定端口 firewall-cmd --zone=public --remove-port=80/tcp --permanent systemctl restart f ...

  7. 力扣1068(MySQL)-产品销售分析Ⅰ(简单)

    题目: 销售表 Sales: 产品表 Product: 写一条SQL 查询语句获取 Sales 表中所有产品对应的 产品名称 product_name 以及该产品的所有 售卖年份 year 和 价格 ...

  8. 阿里 Seata 新版本终于解决了 TCC 模式的幂等、悬挂和空回滚问题

    简介: 今天来聊一聊阿里巴巴 Seata 新版本(1.5.1)是怎么解决 TCC 模式下的幂等.悬挂和空回滚问题的. 作者:朱晋君   大家好,我是君哥. 今天来聊一聊阿里巴巴 Seata 新版本(1 ...

  9. MaxCompute在电商场景中如何进行漏斗模型分析

    简介: 本文以某电商案例为例,通过案例为您介绍如何使用离线计算并制作漏斗图. 背景 漏斗模型其实是通过产品各项数据的转化率来判断产品运营情况的工具.转化漏斗则是通过各阶段数据的转化,来判断产品在哪一个 ...

  10. Snowflake核心技术解读系列——架构设计

    ​简介:Snowflake取得了巨大的商业成功,技术是如何支撑起它的千亿美元市值呢?它技术强在哪?本文为大家倾情解读Snowflake的核心技术原理. 背景:2020年9月16日,Snowflake成 ...