经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 软件/图像 » unity » 查看文章
[Unity] 实现AssetBundle资源加载管理器
来源:cnblogs  作者:千松  时间:2024/5/24 10:26:58  对本文有异议

实现Unity AssetBundle资源加载管理器

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

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

1 成员定义与初始化

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

  1. // Mono单例基类
  2. public abstract class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
  3. {
  4. private static T _instance;
  5. public static T Instance
  6. {
  7. get
  8. {
  9. if (_instance == null)
  10. {
  11. // 在场景中查找是否已存在该类型的实例
  12. _instance = FindObjectOfType<T>();
  13. // 如果场景中不存在该类型的实例,则创建一个新的GameObject并添加该组件
  14. if (_instance == null)
  15. {
  16. GameObject singletonObject = new GameObject(typeof(T).Name + "(Singleton)");
  17. DontDestroyOnLoad(singletonObject); // 保留在场景切换时不被销毁
  18. _instance = singletonObject.AddComponent<T>();
  19. }
  20. }
  21. return _instance;
  22. }
  23. }
  24. }

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

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

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

  1. private static readonly string MAIN_AB_NAME = // 主包名
  2. #if UNITY_IOS
  3. "iOS";
  4. #elif UNITY_ANDROID
  5. "Android";
  6. #else
  7. "PC";
  8. #endif

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

  1. public class ABManager : SingletonMono<ABManager>
  2. {
  3. // ......
  4. private AssetBundleManifest _mainManifest;
  5. private void Awake()
  6. {
  7. // 加载主包的manifest
  8. AssetBundle mainAssetBundle = AssetBundle.LoadFromFile(AB_DIR + MAIN_AB_NAME);
  9. _mainManifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  10. mainAssetBundle.Unload(false); // 加载完manifest之后就可以释放主包
  11. }
  12. // ......
  13. }

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

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

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

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

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

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

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

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

  1. public class ABManager : SingletonMono<ABManager>
  2. {
  3. private static readonly string AB_DIR = Application.streamingAssetsPath + '/'; // AB包所在目录
  4. private static readonly string MAIN_AB_NAME = // 主包名
  5. #if UNITY_IOS
  6. "iOS";
  7. #elif UNITY_ANDROID
  8. "Android";
  9. #else
  10. "PC";
  11. #endif
  12. private AssetBundleManifest _mainManifest;
  13. private readonly Dictionary<string, AssetBundle> _assetBundles = new();
  14. private readonly Dictionary<string, ABStatus> _loadingStatus = new();
  15. private void Awake()
  16. {
  17. // 加载主包的manifest
  18. AssetBundle mainAssetBundle = AssetBundle.LoadFromFile(AB_DIR + MAIN_AB_NAME);
  19. _mainManifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  20. mainAssetBundle.Unload(false); // 加载完manifest之后就可以释放主包
  21. }
  22. // ......
  23. }

2 卸载AB包

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

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

  1. public void Unload(string abName, bool unloadAllLoadedObjects = false)
  2. {
  3. if (!_assetBundles.ContainsKey(abName) || _assetBundles[abName] == null)
  4. {
  5. return;
  6. }
  7. _assetBundles[abName].Unload(unloadAllLoadedObjects);
  8. _assetBundles.Remove(abName);
  9. _loadingStatus.Remove(abName);
  10. }

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

  1. public void UnloadAllAssetBundles(bool unloadAllLoadedObjects = false)
  2. {
  3. _assetBundles.Clear();
  4. _loadingStatus.Clear();
  5. AssetBundle.UnloadAllAssetBundles(unloadAllLoadedObjects);
  6. }

3 同步加载

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

  1. private ABStatus _checkStatus(string abName)
  2. {
  3. return _loadingStatus.TryGetValue(abName, out ABStatus value)
  4. ? value : ABStatus.NotLoaded;
  5. }
  6. private void _setStatus(string abName, ABStatus status)
  7. {
  8. _loadingStatus[abName] = status;
  9. }

3.1 同步加载AB包

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

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

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

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

  1. private void _loadAssetBundle(string abName)
  2. {
  3. Queue<string> loadQueue = new();
  4. loadQueue.Enqueue(abName);
  5. for (; loadQueue.Count > 0; loadQueue.Dequeue())
  6. {
  7. string name = loadQueue.Peek();
  8. // 跳过已完成的包
  9. if (_checkStatus(name) == ABStatus.Completed)
  10. {
  11. continue;
  12. }
  13. // 打断正在异步加载的包
  14. if (_checkStatus(name) == ABStatus.Loading)
  15. {
  16. Unload(name);
  17. }
  18. // 同步方式加载AB包
  19. _assetBundles[name] = AssetBundle.LoadFromFile(AB_DIR + name);
  20. if (_assetBundles[name] == null)
  21. {
  22. throw new ArgumentException($"AssetBundle '{name}' 加载失败");
  23. }
  24. _setStatus(name, ABStatus.Completed);
  25. // 添加依赖包到待加载列表中
  26. foreach (var depend in _mainManifest.GetAllDependencies(name))
  27. {
  28. loadQueue.Enqueue(depend);
  29. }
  30. }
  31. }

3.2 同步加载资源

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

  1. public T LoadRes<T>(string abName, string resName) where T : UnityEngine.Object
  2. {
  3. if (_checkStatus(abName) != ABStatus.Completed)
  4. {
  5. _loadAssetBundle(abName);
  6. }
  7. T res = _assetBundles[abName].LoadAsset<T>(resName);
  8. if (res == null)
  9. {
  10. throw new ArgumentException($"无法从AssetBundle '{abName}' 中获取资源 '{resName}'。");
  11. }
  12. return res;
  13. }

注意
这里不要缩写成 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),这里完全可以用来模拟这一过程。

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

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

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

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

  1. private IEnumerator _loadAssetBundleAsync(string abName)
  2. {
  3. HashSet<string> visitedBundles = new() { abName };
  4. Stack<(string name, bool needAddDepends)> loadStack = new();
  5. loadStack.Push((abName, true));
  6. while (loadStack.Count > 0)
  7. {
  8. var (name, needAddDepends) = loadStack.Peek();
  9. // 跳过已完成的包
  10. if (_checkStatus(name) == ABStatus.Completed)
  11. {
  12. loadStack.Pop();
  13. continue;
  14. }
  15. // 暂停等待正在加载的包
  16. if (_checkStatus(name) == ABStatus.Loading)
  17. {
  18. yield return null;
  19. continue;
  20. }
  21. // 先处理依赖包
  22. if (needAddDepends)
  23. {
  24. loadStack.Pop();
  25. loadStack.Push((name, false));
  26. foreach (var depend in _mainManifest.GetAllDependencies(name))
  27. {
  28. if (visitedBundles.Add(depend))
  29. {
  30. loadStack.Push((depend, true));
  31. }
  32. }
  33. continue;
  34. }
  35. // 异步加载AB包
  36. AssetBundleCreateRequest abCreateRequest = AssetBundle.LoadFromFileAsync(AB_DIR + name);
  37. _assetBundles[name] = abCreateRequest.assetBundle;
  38. _setStatus(name, ABStatus.Loading);
  39. if (_assetBundles[name] == null)
  40. {
  41. throw new ArgumentException($"AssetBundle '{name}' 加载失败");
  42. }
  43. yield return abCreateRequest;
  44. // 加载完成
  45. _setStatus(name, ABStatus.Completed);
  46. }
  47. }

4.2 异步加载资源

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

  1. private IEnumerator _loadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : UnityEngine.Object
  2. {
  3. // 等待异步加载AB包
  4. if (_checkStatus(abName) != ABStatus.Completed)
  5. {
  6. yield return StartCoroutine(_loadAssetBundleAsync(abName));
  7. }
  8. // 异步加载资源
  9. AssetBundleRequest abRequest = _assetBundles[abName].LoadAssetAsync<T>(resName);
  10. yield return abRequest;
  11. T res = abRequest.asset as T;
  12. // 错误处理:资源不存在
  13. if (res == null)
  14. {
  15. throw new ArgumentException($"无法从AssetBundle '{abName}' 中获取资源 '{resName}'。");
  16. }
  17. // 回调
  18. callBack(res);
  19. }
  20. public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : UnityEngine.Object
  21. {
  22. StartCoroutine(_loadResAsync<T>(abName, resName, callBack));
  23. }

5 完整代码

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using Unity.VisualScripting;
  5. using UnityEngine;
  6. using UnityEngine.Events;
  7. using Object = UnityEngine.Object;
  8. enum ABStatus
  9. {
  10. Completed, // 本包和依赖包都加载完毕
  11. Loading, // 正在加载
  12. NotLoaded // 未被加载
  13. }
  14. public class ABManager : SingletonMono<ABManager>
  15. {
  16. private static readonly string AB_DIR = Application.streamingAssetsPath + '/'; // AB包所在目录
  17. private static readonly string MAIN_AB_NAME = // 主包名
  18. #if UNITY_IOS
  19. "iOS";
  20. #elif UNITY_ANDROID
  21. "Android";
  22. #else
  23. "PC";
  24. #endif
  25. private AssetBundleManifest _mainManifest;
  26. private readonly Dictionary<string, AssetBundle> _assetBundles = new();
  27. private readonly Dictionary<string, ABStatus> _loadingStatus = new();
  28. private void Awake()
  29. {
  30. // 加载主包的manifest
  31. AssetBundle mainAssetBundle = AssetBundle.LoadFromFile(AB_DIR + MAIN_AB_NAME);
  32. _mainManifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  33. mainAssetBundle.Unload(false); // 加载完manifest之后就可以释放主包
  34. }
  35. private ABStatus _checkStatus(string abName)
  36. {
  37. return _loadingStatus.TryGetValue(abName, out ABStatus value)
  38. ? value : ABStatus.NotLoaded;
  39. }
  40. private void _setStatus(string abName, ABStatus status)
  41. {
  42. _loadingStatus[abName] = status;
  43. }
  44. private void _loadAssetBundle(string abName)
  45. {
  46. Queue<string> loadQueue = new();
  47. loadQueue.Enqueue(abName);
  48. for (; loadQueue.Count > 0; loadQueue.Dequeue())
  49. {
  50. string name = loadQueue.Peek();
  51. // 跳过已完成的包
  52. if (_checkStatus(name) == ABStatus.Completed)
  53. {
  54. continue;
  55. }
  56. // 打断正在异步加载的包
  57. if (_checkStatus(name) == ABStatus.Loading)
  58. {
  59. Unload(name);
  60. }
  61. // 同步方式加载AB包
  62. _assetBundles[name] = AssetBundle.LoadFromFile(AB_DIR + name);
  63. if (_assetBundles[name] == null)
  64. {
  65. throw new ArgumentException($"AssetBundle '{name}' 加载失败");
  66. }
  67. _setStatus(name, ABStatus.Completed);
  68. // 添加依赖包到待加载列表中
  69. foreach (var depend in _mainManifest.GetAllDependencies(name))
  70. {
  71. loadQueue.Enqueue(depend);
  72. }
  73. }
  74. }
  75. public T LoadRes<T>(string abName, string resName) where T : Object
  76. {
  77. if (_checkStatus(abName) != ABStatus.Completed)
  78. {
  79. _loadAssetBundle(abName);
  80. }
  81. T res = _assetBundles[abName].LoadAsset<T>(resName);
  82. if (res == null)
  83. {
  84. throw new ArgumentException($"无法从AssetBundle '{abName}' 中获取资源 '{resName}'。");
  85. }
  86. return res;
  87. }
  88. private IEnumerator _loadAssetBundleAsync(string abName)
  89. {
  90. HashSet<string> visitedBundles = new() { abName };
  91. Stack<(string name, bool needAddDepends)> loadStack = new();
  92. loadStack.Push((abName, true));
  93. while (loadStack.Count > 0)
  94. {
  95. var (name, needAddDepends) = loadStack.Peek();
  96. // 跳过已完成的包
  97. if (_checkStatus(name) == ABStatus.Completed)
  98. {
  99. loadStack.Pop();
  100. continue;
  101. }
  102. // 暂停等待正在加载的包
  103. if (_checkStatus(name) == ABStatus.Loading)
  104. {
  105. yield return null;
  106. continue;
  107. }
  108. // 先处理依赖包
  109. if (needAddDepends)
  110. {
  111. loadStack.Pop();
  112. loadStack.Push((name, false));
  113. foreach (var depend in _mainManifest.GetAllDependencies(name))
  114. {
  115. if (visitedBundles.Add(depend))
  116. {
  117. loadStack.Push((depend, true));
  118. }
  119. }
  120. continue;
  121. }
  122. // 异步加载AB包
  123. AssetBundleCreateRequest abCreateRequest = AssetBundle.LoadFromFileAsync(AB_DIR + name);
  124. _assetBundles[name] = abCreateRequest.assetBundle;
  125. _setStatus(name, ABStatus.Loading);
  126. if (_assetBundles[name] == null)
  127. {
  128. throw new ArgumentException($"AssetBundle '{name}' 加载失败");
  129. }
  130. yield return abCreateRequest;
  131. // 加载完成
  132. _setStatus(name, ABStatus.Completed);
  133. }
  134. }
  135. private IEnumerator _loadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
  136. {
  137. // 等待异步加载AB包
  138. if (_checkStatus(abName) != ABStatus.Completed)
  139. {
  140. yield return StartCoroutine(_loadAssetBundleAsync(abName));
  141. }
  142. // 异步加载资源
  143. AssetBundleRequest abRequest = _assetBundles[abName].LoadAssetAsync<T>(resName);
  144. yield return abRequest;
  145. T res = abRequest.asset as T;
  146. // 错误处理:资源不存在
  147. if (res == null)
  148. {
  149. throw new ArgumentException($"无法从AssetBundle '{abName}' 中获取资源 '{resName}'。");
  150. }
  151. // 回调
  152. callBack(res);
  153. }
  154. public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
  155. {
  156. StartCoroutine(_loadResAsync<T>(abName, resName, callBack));
  157. }
  158. public void Unload(string abName, bool unloadAllLoadedObjects = false)
  159. {
  160. if (!_assetBundles.ContainsKey(abName) || _assetBundles[abName] == null)
  161. {
  162. return;
  163. }
  164. _assetBundles[abName].Unload(unloadAllLoadedObjects);
  165. _assetBundles.Remove(abName);
  166. _loadingStatus.Remove(abName);
  167. }
  168. public void UnloadAllAssetBundles(bool unloadAllLoadedObjects = false)
  169. {
  170. _assetBundles.Clear();
  171. _loadingStatus.Clear();
  172. AssetBundle.UnloadAllAssetBundles(unloadAllLoadedObjects);
  173. }
  174. }

参考资料

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

Custom == operator, should we keep it?

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


本文发布于2024年5月23日

最后编辑于2024年5月23日

原文链接:https://www.cnblogs.com/ThousandPine/p/18208153

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号