赞
踩
这篇文章内容巨多,逻辑也复杂,花了4天写出来。(写博客还是费时间啊)
很多设计和逻辑,在脑子中是很清晰的,但用文字表述就会显得很复杂,没有图文对照就更难理解了。
Unity资源类型,按加载流程顺序,有三种
AssetBundle
资源以压缩包文件存在(Resources目录下资源打成包体后也是以ab格式存在)Asset
资源在内存中的存在格式GameObject
针对Prefab导出的Asset,可实例化针对AssetBundle
的加载,本文会作讲解,并提供整套方案和代码,
针对Asset
的加载,读者可以参阅Asset同步异步引用计数资源加载管理器
针对GameObject
的加载,读者可以参阅Prefab加载自动化管理引用计数管理器
AssetBundle加载有三套接口,WWW
,UnityWebRequest
和AssetBundle
,大部分文章都推荐AssetBundle
,本人也推荐。
关于AssetBundle的加载原理和用法之类的基础知识读者自己百度学习,这边就不进行大量描述了
前两者都要经历将整个文件的二进制流下载或读取到内存中,然后对这段内存文件进行ab资源的读取解析操作,而AssetBundle
可以只读取存储于本地的ab文件的头部部分,在需要的情况下,读取ab中的数据段部分(Asset资源)。
所以AssetBundle
相对的优势是
所以,从内存和效率方面,AssetBundle
会是目前最优解,而使用非压缩或LZ4读者自己评断(推荐LZ4)
AssetBundle
加载方式最重要的接口(接口用法读者自己百度学习)
AssetBundle.LoadFromFile 从本地文件同步加载ab
AssetBundle.LoadFromFileAsync 从本地文件异步加载ab
AssetBundle.Unload 卸载,注意true和false区别
AssetBundle.LoadAsset 从ab同步加载Asset
AssetBundle.LoadAssetAsync 从ab异步加载Asset
使用异步AssetBundle
加载的时候,大部分开发者都喜欢使用协程的方式去加载,当然这已经成为通用做法。但这种做法弊端也很明显:
大量依赖ab等待加载,逻辑复杂
ab加载状态切换的复杂化
协程顺序的不确定性,增加难度
ab卸载和加载同时进行处理难
ab同步和异步同时进行处理难
协程在某些情况确实可以让开发简单化,但在耦合高的代码中非常容易导致逻辑复杂化。
这里笔者提供一种使用Update去协程化的方案。
我们都知道,使用协程的地方,大部分都是需要等待线程返回逻辑的,而这样的等待逻辑可以使用Update每帧访问的方式,确定线程逻辑是否结束
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path); IEnumerator LoadAssetBundle() { yield return request; //do something } 转变为 void Update() { if(request.isDone) { //do something } }
其实协程本质,就是保留现场的回调函数,内部机制也是update的每帧遍历(具体参见IEnumerator
原理)。
既然是加载资源,那必然会有队列,笔者这边依据需求和优化要求,设计成四个队列,准备队列
、加载队列
、完成队列
和销毁队列
。
代码如下
private Dictionary<string, AssetBundleObject> _readyABList; //预备加载的列表
private Dictionary<string, AssetBundleObject> _loadingABList; //正在加载的列表
private Dictionary<string, AssetBundleObject> _loadedABList; //加载完成的列表
private Dictionary<string, AssetBundleObject> _unloadABList; //准备卸载的列表
队列之间,队列成员的转移需要一个触发点,而这样的触发点如果都写在加载和销毁逻辑里,耦合度过高,而且逻辑复杂还容易出错。
TIP:为什么没有设计异常队列
?
- 一般资源加载,都是默认资源是存在的
- 资源如果不存在,一定是策划没有把资源放进去(嗯,一定是这样)
- 设计上是加载了总依赖关系的Mainfest,是对文件存在性可以进行判断的
- 从性能的角度,通过
File.exists()
来判断文件存在性,是效率低下的方式- 代码中对异常是有处理的,会有重复加载,下载和修复完整性的逻辑
笔者很喜欢的一种设计,就是通过Update来降低耦合度
,这种方式代码清晰,逻辑简单,但缺点也很明显,丢失原始现场。
回到本篇文章,当然是通过Update来运行逻辑,如下
TIP:为什么Update里三个函数的运行顺序跟队列转移顺序不一样?
- UpdateReady在UpdateLoad后面,可以实现当前帧就创建新的加载,否则要等到下一帧
- UpdateUnLoad放最后,是因为正在加载的资源要等到加载完才能卸载
根据上面的逻辑,很容易设计下面的接口逻辑
LoadMainfest
是用来加载文件列表和依赖关系的,一般在游戏热更之后,游戏登录界面之前进行游戏初始化的时候。加载的配置文件是Unity导出AssetBundle时生成的主Mainfest文件,具体逻辑如下
_dependsDataList.Clear(); AssetBundle ab = AssetBundle.LoadFromFile(path); AssetBundleManifest mainfest = ab.LoadAsset("AssetBundleManifest") as AssetBundleManifest; foreach(string assetName in mainfest.GetAllAssetBundles()) { string hashName = assetName.Replace(".ab", ""); string[] dps = mainfest.GetAllDependencies(assetName); for (int i = 0; i < dps.Length; i++) dps[i] = dps[i].Replace(".ab", ""); _dependsDataList.Add(hashName, dps); } ab.Unload(true); ab = null;
这部分,大部分游戏都大同小异,就是将配置转化成类结构。注意ab.Unload(true);
用完要销毁。
public delegate void AssetBundleLoadCallBack(AssetBundle ab); private class AssetBundleObject { public string _hashName; //hash标识符 public int _refCount; //引用计数 public List<AssetBundleLoadCallBack> _callFunList = new List<AssetBundleLoadCallBack>(); //回调函数 public AssetBundleCreateRequest _request; //异步加载请求 public AssetBundle _ab; //加载到的ab public int _dependLoadingCount; //依赖计数 public List<AssetBundleObject> _depends = new List<AssetBundleObject>(); //依赖项 }
加载节点的数据结构不复杂,看代码就很容易理解。
依赖加载,是ab加载逻辑里最难最复杂最容易出bug的地方,也是本文的难点。
难点为一下几点:
还未加载
,准备加载
,正在加载
,已经加载
节点关系处理我们来一一分解
首先,看一下ab节点的引用计数要实现的逻辑
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。