实现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. Docker学习路线11:Docker命令行

    Docker CLI (命令行界面) 是一个强大的工具,可让您与 Docker 容器.映像.卷和网络进行交互和管理.它为用户提供了广泛的命令,用于在其开发和生产工作流中创建.运行和管理 Docker ...

  2. C#对接部标JT808协议实现北斗定位设备数据接收服务端

    一.前言介绍 开发一套能够支撑几万台北斗定位设备数据接收的服务端,用于接收北斗定位器定位数据的平台.项目基于windows平台,C#语言开发框架Net Framework4.8,TCP主要基于Supe ...

  3. windows创建bat文件进行截图

    1.创建 bat 文件 2.编辑文件内容 start snippingtool

  4. 7月27日19:30直播预告:HarmonyOS3及华为全场景新品发布会

    7月27日 19:30 HarmonyOS 3 及华为全场景新品发布会 高能来袭! 在HarmonyOS开发者社区企微直播间 一起见证HarmonyOS的又一次智慧进化 扫码预约直播,与您不见不散!

  5. Mysql之主从异步

    数据库创建完后主从数据库数据保持同步 主数据库 mysql> SHOW MASTER STATUS; +------------------+----------+--------------+ ...

  6. redis 简单整理——缓存设计[三十二]

    前言 简单整理一下缓存设计. 正文 缓存的好处: ·加速读写:因为缓存通常都是全内存的(例如Redis.Memcache),而 存储层通常读写性能不够强悍(例如MySQL),通过缓存的使用可以有效 地 ...

  7. Python - PEP572: 海象运算符

    海象运算符 PEP572 的标题是「Assignment Expressions」,也就是「赋值表达式」,也叫做「命名表达式」 不过它现在被广泛的别名是「海象运算符」(The Walrus Opera ...

  8. pytorch,numpy两种方法实现nms类间+类内

    类间:也就是不同类之间也进行nms 类内:就是只把同类的bboxes进行nms numpy实现 nms类间+类内: import numpy as np # 类间nms def nms(bboxes, ...

  9. 红日安全vulnstack (一)

    网络拓扑图 靶机参考文章 CS/MSF派发shell 环境搭建 IP搭建教程 本机双网卡 65网段和83网段是自己本机电脑(虚拟机)中的网卡, 靶机外网的IP需要借助我们这两个网段之一出网 Kali ...

  10. 测试环境不稳定&复杂的必然性及其对策

    简介: 为什么测试环境的不稳定是必然的,怎么让它尽量稳定一点?为什么测试环境比生产环境更复杂,怎么让它尽量简单一点?本文将就这两点进行分享.同时,还会谈一谈对测试环境和生产环境的区别的理解. 作者 | ...