概要
对于游戏开发来说,资源管理是必不可少的。Unity中使用AssetBundle是大部分开发者的选择,但是使用官方的AB标签形式去打包的话,需要管理每个资产的标签,这有些分散与不易不易组织。而AddressableAsset目前没看到大规模使用的资料,而Unity的新技术总是没那么让人放心。所以最终我决定仿照大部分的项目自己写一个AB打包以及加载的模块来处理资源问题。
设计与实现
打包
打包大部分为编辑器代码。分为打包器配置、打包配置、打包三个部分。各种配置以及存储使用Json实现,也可以使用ScriptableAsset,但Json对字典等的序列化支持的更好。
打包器配置
打包器的配置没什么特别的,就是确认各种路径以及忽略项目之类的。
/// <summary>
/// AB配置数据类
/// </summary>
[System.Serializable]
public class GeneratorCfgData
{
public string ABExtension = ".ab";
public readonly string ABPackageCfgsJsonPath = Path.Combine(Application.dataPath, "Editor", "ABCfg", "ABCfg.json");
public readonly string ABStorePath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "AssetsBundle");//打包结果存储位置
public readonly string ABCopyPath = Path.Combine(Application.streamingAssetsPath, "AssetsBundle");//打包结果拷贝存储位置
public bool isWarnAutoDepBundleSize = true;//是否警告自动依赖收集包大小
public float warnAutoDepBundleFileSize_KB = 1024.0f;//警告自动依赖收集包大小(单位KB)
///获取文件时 需要忽略的文件后缀名
public List<string> notCollectFileExtensions = new List<string>();
///获取文件时 需要忽略的文件夹
public List<string> notCollectDirFullName = new List<string>();
///收集依赖时 需要忽略的文件后缀名
public List<string> notCollectFileExtensions_Dependencies = new List<string>();
///收集依赖时 需要忽略的被依赖的文件后缀名
public List<string> notCollectFileExtensions_Dependencies_Dep= new List<string>();
///打包选项
public BuildAssetBundleOptions buildAssetBundleOptions = BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.ForceRebuildAssetBundle;
///打包目标平台
public BuildTarget buildTarget = EditorUserBuildSettings.activeBuildTarget;
///是否清空输出目录
public bool isCleanOutputDir = true;
///拷贝到SteamingAssets
public bool copy2SteamingAssets = false;
}
打包配置
打包的的配置用来确定在什么范围内生成一个或多个AB。
/// <summary>
/// AB包配置数据类
/// </summary>
[System.Serializable]
public class ABPackageCfgData
{
public string name;//此规则的名字
public bool isUsing = true;//是否启用此打包规则
public ePackWay packWay = ePackWay.none;//打包方式
public string assetDir;//资源的搜索根目录
public string searchePattern;//搜索通配符
public bool isRecursionSearch = true;//是否递归的搜索(total不适用)
public bool isForceIncludeDepends = false;//是否强制包含依赖(优先用它)
public bool isIncludeTopDir = false;//是否包含顶层目录(eachFolder适用)
}
/// <summary>
/// 打包方式
/// </summary>
public enum ePackWay
{
none = 0,//未定义
eachFile = 1,//每个文件一个ab
eachFolder = 2,//每个文件夹一个ab
total = 3,//整个根目录一个ab
}
打包
打包的主要流程如StartGenABs()函数所示,我会先生成一份中间数据来把打包配置对应的一个或多个AB的信息生成出来,中间数据包括AB包名字、所有资源路径、依赖的其它AB名字(稍后生成)等。
生成了中间数据之后是去收集依赖,确认AB之间的依赖关系,如果出现了没有在任何AB中的依赖资源,那就把这些依赖单独生成一个中间数据。
有了中间数据之后,把它转换成Unity内部的AssetBundleBuild对象,用于给打包管线打包使用。
public static void StartGenABs()
{
////防止没有保存
//SaveGeneratorCfg();
//SaveABCfgs();
try
{
//生成中间数据
List<ABMidData> midDatas = GenABMidData();
//处理收集依赖
CollectDependencies(midDatas);
//生成最终的unity用的打包配置
List<AssetBundleBuild> ABBList = GenRealABBuilData(midDatas);
//打包AB包
BuildAB(ABBList);
}
catch (CancleBuildABException e)
{
Debug.LogWarning(e);
throw;
}
EditorUtility.ClearProgressBar();//关掉进度条
}
其中,收集依赖主要靠AssetDatabase.GetDependencies(string pathName)函数实现,它能获得Unity内部的资源索引。
在打包的最后阶段,我会生成一个自定义的清单文件,把资源到AB的关系映射出来,在加载资源时将会用到它来查找资源属于哪一个AB。
Tip:这里存在一个性能方面的疑虑,自定义清单可能会非常大。
加载
类说明
每一个真正的Asset都对应一个ResBase类,这些ResBase由ResLoader管理,而总管理器ResLoadManager又会管理所有的ResLoader以及AB。
继承ResBase的类目前有三个:EditorRes、BundleRes、BundleAssetRes。
EditorRes是编辑器下用的资源类,使用AssetDatabase.LoadAssetAtPath。
BundleRes是直接加载AB的资源类。
BundleAssetRes是加载AB中资源的资源类。
为了开发的方便,我希望资源的加载可以在AB模式以及非AB下无感的切换。
为此,使用UNITY_EDITOR宏来生成不同ResBase,编辑器下使用EditorRes,其它情况则使用AB。
异步加载
本来想使用Async与Awit来做异步,但是Unity本身的异步代码都是迭代器的协程异步,没法与Async配合。所以就写了一个协程管理,迭代器都会被存到一个队列中,上一个跑完了跑下一个以此来实现异步。
AB管理
每一个AB都会有引用计数;在加载时,AB及其依赖AB的引用计数会+1,卸载时-1。当减到0时则unload此AB。
异常处理
当Resloader被回收的时候,假如资源还没加载完或者加载到一半(异步),需要处理异常。
public void OnRecycle()
{
//释放资源
if (allRes.Count > 0)
{
foreach (IRes res in allRes)
{
if (res.resState == eResState.loadDone)
{
res.ReleaseAsset();
}
else if (res.resState == eResState.wait2Load)//协程Action加到队列里了
{
res.RemoveFromeWait2LoadCoroutieQueeu();
}
else if (res.resState == eResState.loading)//协程开始跑了
{
res.StopResLoadAsyncRoutine();
}
}
allRes.Clear();
}
}
/// <summary>
/// 从等待加载的队列中移除
/// </summary>
public void RemoveFromeWait2LoadCoroutieQueeu()
{
if (loadAction != null)
{
ResLoadManager.RemoveWait2LoadCoroutine(loadAction);
}
}
/// <summary>
/// 停止加载的协程
/// </summary>
public void StopResLoadAsyncRoutine()
{
if (loadCoroutine != null)
{
//直接停止可能会因为调用的不原子而产生问题
//比如AB加载好了,但是引用的AB没加载好
//MonoDriver.Instance.StopCoroutine(loadCoroutine);
//替换掉LoadOver
finishAction = null;
loadOverCallback = () => {
ReleaseAsset();
};
}
}
场景加载
这里不太清楚原理,还需要再学习。目前的用法是一个场景一个AB,在场景管理器里每当要加载场景就新获取一个ResLoader然后将他们存入字典;监听unloadScene这个事件,当这个场景被回收时,同时回收对应的ResLoader来保证AB的回收。
图集加载
图集的主动加载需要先手动加载图集,然后从这个图集里获取sprite。直接在场景里的sprite可以正确的读取到对应的图集。
总结
经过测试,在加载场景、切换场景、加载预制体、删除预制体时能够正确的读取AB以及释放AB。同时图集的合批也没有问题。
使用AssetBundle Browser对AB进行检视,图集也没有出现资源重复的问题。
目前来说,这个打包加载模块足够我自己的开发使用了。
参考:
[官网资源工作流]https://docs.unity3d.com/cn/current/Manual/AssetWorkflow.html