[Unity] 自定义AssetsManager进行资源加载与管理

[Unity] 自定义AssetsManager进行资源加载与管理

问题的产生:

  1. 游戏通过AssetBundle读取资源,如预制体、图片等

  2. AssetBundle有很多,并且Bundle之间可能存在依赖,如Bundle A中的预制体使用了Bundle B中的资源

  3. 同步加载可能造成进程卡死,如资源过大,解压速度慢等

思路:

  1. 使用协程进行异步加载

  2. 加载资源前,先检查Bundle是否被加载,如果没有被加载则先加载Bundle

  3. 加载Bundle前,先检查是否依赖其他Bundle,如果有有依赖与其他则先加载其他Bundle

  4. Bundle加载完毕,再加载资源即可

本文实现:

准备:

  1. 首先准备一个记录资源映射的json文件,如下图,key值为资源的路径,value值为资源所在的Bundle名称。
    在这里插入图片描述

  2. 建立一个ResConst.cs类,将Bundle中的资源路径填写进去,需要注意的是,此处的路径需要与json文件的key值一致,如下图。
    在这里插入图片描述

实现

  1. 新建一个AssetsManager.cs作为资源管理器(不一定要叫这个名啊),需要注意的是因为需要使用协程,因此AssetsManager是需要继承自MonoBehavior的,主要有三个字段

    public class AssetsManager : MonoBehaviour
    {
        // 资源路径 -> Bundle名称
        private Dictionary<string, string> path2bundle = new Dictionary<string, string>();
        // Bundle名称 -> AssetBundle
        private Dictionary<string, AssetBundle> bundles = new Dictionary<string, AssetBundle>();
        // 主Bundle的manifest文件
        private AssetBundleManifest mainManifest;  
    }
    
  2. 首先读取主Bundle和它的.manifest,也就是下图这两个文件(在Awake中执行)
    在这里插入图片描述

    • 主Bundle的名称就为导出Bundle时,导出文件夹的名称(此处就叫Bundle)

    • 主Bundle的.manifest文件记录了所有Bundle以及其依赖,如下图,红色箭头所指Bundle的名称以及紫色方框中就为其依赖的Bundle
      在这里插入图片描述

    • 将主Bundle的manifest文件读取出来,需要注意的是主Bundle的manifest就叫AssetBundleManifest

      // 初始化主Bundle 方便后续拿依赖
      // 主AB包一定存在,而且与导出时的文件夹名相同
      DirectoryInfo directory = new DirectoryInfo(GlobalConfig.AssetBundleDir);
      string mainPath = Path.Combine(GlobalConfig.AssetBundleDir, directory.Name);
      AssetBundle main = AssetBundle.LoadFromFile(mainPath);
      // 获取主AB包的配置文件
      mainManifest = main.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
      
  3. 利用LitJson读取出来并存储到字典中,此处我将资源映射文件AssetMapping.json打到名称为main的Bundle中。(在Awake中执行)

     // 把资源路径与bundle映射先加载出来
     string mappingFile = Path.Combine(GlobalConfig.AssetBundleDir, "main");
     AssetBundle bundle = AssetBundle.LoadFromFile(mappingFile);
     // 加入到Bundle字典中
     bundles.Add("main", bundle);
     TextAsset mappingText = bundle.LoadAsset<TextAsset>("AssetMapping.json");
     JsonReader reader = new JsonReader(mappingText.ToString());
     JsonData data = JsonMapper.ToObject(reader);
     foreach (string key in data.Keys)
     {
         path2bundle.Add(key, data[key].ToString());
     }
     reader.Close();
    
  4. 实现加载Bundle。

    • 先检查依赖

    • 如果没有依赖了,则代表所有包都已经加载完毕,就返回

    • 否则开启协程加载所有包

    private void LoadAssetBundle(string bundleName, Action<AssetBundle> onLoaded)
    {
        // 检查出不存在的依赖
        List<string> deps = CheckNoneExistedDependencies(bundleName);
        // 如果没有不存在的直接返回
        if (deps.Count == 0)
        {
            onLoaded?.Invoke(bundles[bundleName]);
        }
        else
        {
            // 异步加载
            List<object> parameters = new List<object>()
            {
                deps,
                bundleName,
                onLoaded,
            };
            StartCoroutine(nameof(LoadAssetBundlesAsync), parameters);
        }
    }
    
  5. 协程LoadAssetBundlesAsync的实现,这一步的关键在于if条件为ture的分支中防止重复加载的yield return语句和false分支中占位的bundles.Add(deps[i], null)语句,主要是基于以下考量

    • 游戏资源加载是非常频繁的,当同一时间(帧)多个协程加载同一个还未加载的Bundle,如果不控制重复加载,就会报The AssetBundle XXX can't be loaded because another AssetBundle with the same files is already loaded
    • 先占位,表明已经有协程在加载这个Bundle了,其他协程到这里只需要等待即可
IEnumerator LoadAssetBundlesAsync(List<object> obj)
{
    List<string> deps = (List<string>)obj[0];
    string targetBundleName = (string)obj[1];
    Action<AssetBundle> onLoaded = (Action<AssetBundle>)obj[2];
    for (int i = 0; i < deps.Count; i++)
    {
        if (bundles.ContainsKey(deps[i]))
        {
            // 防止重复加载
            yield return new WaitUntil(() => bundles[deps[i]] != null);
        }
        else
        {
            string bundlePath = Path.Combine(GlobalConfig.AssetBundleDir, deps[i]);
            // 占位
            bundles.Add(deps[i], null);
            AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(bundlePath);
            yield return request;
            bundles[deps[i]] = request.assetBundle;
        }   
    }
    onLoaded?.Invoke(bundles[targetBundleName]);
}
  1. 检查依赖CheckNoneExistedDependencies的实现,函数LoadAssetBundle、协程LoadAssetBundlesAsync和这个函数都是相扣的

    • 使用主Bundle的manifest,调用GetAllDependencies就能拿到该bundle的所有依赖

    • 因为存在占位情况,如果此时需要的bundle没有加载出来则需要加入到不存在列表中

    • 最后检查自己存在与否

private List<string> CheckNoneExistedDependencies(string bundleName)
{
    List<string> noneExisted = new List<string>();
    string[] dependencies = mainManifest.GetAllDependencies(bundleName);
    foreach (string dep in dependencies)
    {
        if (!bundles.ContainsKey(dep) || bundles[dep] == null)
        {
            noneExisted.Add(dep);
        }
    }
    // 也要检查自己存不存在
    if (!bundles.ContainsKey(bundleName) || bundles[bundleName] == null)
    {
        noneExisted.Add(bundleName);
    }
    return noneExisted;
}
  1. 加载资源。当完成上面Bundle的加载,就可以使用ResConst.cs中设定好的路径加载资源了

    public void LoadAsset(string assetPath, Action<UnityEngine.Object> onLoaded = null)
    {
        if (string.IsNullOrEmpty(assetPath) || string.IsNullOrWhiteSpace(assetPath))
        {
            throw new ArgumentException("Bad Argument: It was NULL , Empty or WhiteSpace");
        }
        if (path2bundle.TryGetValue(assetPath, out string bundleName))
        {
            LoadAssetBundle(bundleName, (bundle) =>
            {
                FileInfo file = new FileInfo(assetPath);
                List<object> parameters = new List<object>()
                {
                    file.Name,
                    bundle,
                    onLoaded,
                };
                StartCoroutine(nameof(LoadAssetAsync), parameters);
            });
        }
        else
        {
            throw new DirectoryNotFoundException("Bad Key: " + assetPath + " is not a key of bundle dictionary");
        }
    }
    
    // 调用之前保证Bundle都有
    IEnumerator LoadAssetAsync(List<object> list)
    {
        string assetName = (string)list[0];
        AssetBundle bundle = (AssetBundle)list[1];
        Action<UnityEngine.Object> onLoaded = (Action<UnityEngine.Object>)list[2];
        AssetBundleRequest request = bundle.LoadAssetAsync<UnityEngine.Object>(assetName);
        yield return request;
        onLoaded?.Invoke(request.asset);
    }
    

版权声明:本文为weixin_40885067原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。