Unity Resources资源管理器


文章目录

  • Unity Resources资源管理器
  • 一、目的
  • 二、Resources管理器
  • 1. 路径问题的解决
  • 2. 文件的读取和解析
  • 3. ResourcesManager


一、目的

通常来讲,直接从Resources下进行资源的加载,有几个明显的缺点

  1. 需要指明资源的详细路径,一旦资源位置迁移,代码也要做出相应的改变
  2. 缺少缓存机制,从Resources下加载资源的过程较为耗时,同一个资源只加载一次
  3. 部分API,如异步加载使用起来较为复杂,和游戏逻辑放在一起紧耦合,需要单独封装

Resources资源管理器则是对这些方面进行优化,并加以封装,全局唯一专门负责Resources资源的加载

二、Resources管理器

1. 路径问题的解决

资源的加载,一般路径会因为文件夹结构的调整经常改变,但资源名称很少会进行更改,解决这个问题的核心思想就是如何只用资源名称就能加载到Resources文件夹下指定的资源,可以编写一个Editor工具用来构建资源名称 和 资源路径的映射关系

Editor工具

一般资源类型,比较常见的有prefab预制体,audioClip音频,texture图片等等,利用U3D提供的AssetDatabase下的API可以轻松获取到Resources下指定类型的资源路径,具体见代码注释。

using System.Collections.Generic;
using System.IO;
using UnityEditor;

public class GenrateResConfig : Editor
{
    //映射文件保存位置
    private static string configPath = "Assets/StreamingAssets/ConfigMap.txt";
    //在Unity编辑器界面中 设置一组下拉选项,指定选项路径,点击后就会触发此函数
    [MenuItem("Tools/Resources/Generate ResConfig File")]
    public static void Generate()
    {
        Dictionary<string, List<string>> dic = new Dictionary<string, List<string>>();
        File.Delete(configPath);
        //TODO: 将更多需要保存的资源类型和筛选器加入到此字典中
        //资源类型可以查阅Unity API,根据类型其后缀可能多种多样根据实际资源决定
        dic.Add("prefab", new List<string>() { "prefab" });
        dic.Add("audioclip", new List<string>() { "mp3", "mp4" });
        dic.Add("texture", new List<string>() { "png", "jpg", "bmp" });
        foreach (var item in dic)
        {
            string[] mapArr = getMapping(item.Key,item.Value);
            //3.写入文件
            if (!Directory.Exists("Assets/StreamingAssets"))
            {
                Directory.CreateDirectory("Assets/StreamingAssets");
            }
            File.AppendAllLines(configPath, mapArr);
        }
        //刷新
        AssetDatabase.Refresh();

    }

    private static string[] getMapping(string type,List<string> suffixes)
    {
        //生成资源配置文件
        //1.查找Resources目录下所有预制件的完整路径
        //返回值为GUID 资源编号 , 参数1 指明类型,参数2 在哪些路径下查找
        string[] resFiles = AssetDatabase.FindAssets($"t:{type}", new string[] { "Assets/Resources" });
        for (int i = 0; i < resFiles.Length; i++)
        {
            resFiles[i] = AssetDatabase.GUIDToAssetPath(resFiles[i]);
            //2.生成对应关系  名称=路径
            string fileName = Path.GetFileNameWithoutExtension(resFiles[i]);
            string filePath = resFiles[i].Replace("Assets/Resources/", string.Empty);
            foreach(string filter in suffixes)
            {
                filePath = filePath.Replace("."+filter, string.Empty);
            }
            resFiles[i] = fileName + "=" + filePath;
        }
        return resFiles;
    }
}

注意点

  1. Unity提供的AssetDatabase.FindAssets方法,指定的类型无视大小写,但类型名必须正确,图片texture , 音乐audioclip, 预制体prefab等等。
  2. 最终生成的路径映射和名称,均不带后缀,这也是为什么要添加filter List的原因,应该把此种类型文件的后缀都删除。
  3. 如果除了图片,音乐,预制体有新的资源存储,应在代码的TODO处添加对应新的类型和筛选器列表。

最终能在StreamingAssets文件夹下得到一个ConfigMap.txt如下图样式

unity实现Windows资源管理器 unity资源包管理器_游戏引擎

2. 文件的读取和解析

对于在StreamingAssets下文件的读取,Unity提供了相关的API,可以分行读取,一次性读取等等

通常来讲,配置文件都会按行存储,每一行都会读取出来,并放入合适的数据结构中,在本章中每一行的数据格式为xx = xx/xx/xx,键值对的形式很适合使用Dictionary来存储,下面的ConfigReader类负责按行读取文件,并提供委托可以处理单行的数据,是通用读取类。

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using System;
namespace Common
{
    ///<summary>
    ///负责读取配置文件并提供解析行
    ///<summary>
    public class ConfigReader
    {
        /// <summary>
        /// 加载(获取)配置文件
        /// </summary>
        /// <param name="fileName">文件名</param>
        /// <returns>获取的字符串(待解析)</returns>
        public static string GetConfigFile(string fileName)
        {
            string url;
            //在移动端通过Application.StreamingAssets不靠谱可能会出错 应用以下方法
            //url根据不同平台有不同的路径,利用宏标签在编译期间运行,根据所处平台选择哪条语句
            //发布后相当于就选择了一条合适的语句url=xxxx  
            //if(Application.platform == RuntimePlatform.Android) 性能稍差
#if UNITY_EDITOR || UNITY_STANDALONE
            url = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
#elif UNITY_IPHONE
            url = "file://" + Application.dataPath + "/Raw/"+fileName;
#elif UNITY_ANDROID
            url = "jar:file://" + Application.dataPath + "!/assets/"+fileName;
#endif


            //移动端根据url加载文件资源最终返回一个string
            UnityWebRequest www = UnityWebRequest.Get(url);
            www.SendWebRequest();
            while (true)
            {
                if (www.downloadHandler.isDone)
                    return www.downloadHandler.text;
            }
        }

        //handler委托函数负责处理每行的数据
        public static void Reader(string fileContent,Action<string> handler)
        {

            //读出来的string   "xxxName=xxxPath/r/nxxxName=xxxPath/r/n....
            //解析字符串,利用StringReader字符串读取器,流用完必须释放内存
            //using 代码块结束自动释放资源
            using (StringReader reader = new StringReader(fileContent))
            {
                string line;
                while ((line = reader.ReadLine()) != null) //逐行获取字符串
                {
                    //解析方法
                    handler(line);
                }
            }
        }
    }

}

使用方法如下, 将映射文件解析到Dictionary中便于使用

string fileContent = ConfigReader.GetConfigFile("ConfigMap.txt");
configMap = new Dictionary<string, string>();
//解析文件(string ----> configMap)
ConfigReader.Reader(fileContent, BuildMap);
/// <summary>
/// 负责处理解析每行字符串的功能
/// </summary>
/// <param name="line">每行字符串</param>
private void BuildMap(string line)
{
     string[] keyValue = line.Split('=');
     configMap.Add(keyValue[0], keyValue[1]);
}
3. ResourcesManager

经过创建映射文件,读取映射文件,解析映射文件,现在我们能够得到一个 名字-路径的映射字典,只需要封装一些通用的方法,即很容易的实现通过名称加载资源,在这个管理类中,还需解决缓存防止多次加载的问题,异步加载的协程和委托等等,下面是完整源码。 里面涉及到的MonoSingleton的脚本是Mono单例类,可以自行查阅实现单例类或者阅读 笔者的这篇博客

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

namespace Common
{
    ///<summary>
    ///资源加载管理类
    ///<summary>
    public class ResourcesManager : MonoSingleton<ResourcesManager>
    {
        //负责储存资源的名字和路径映射
        private static Dictionary<string, string> configMap;

        //缓存已经加载的资源
        private static Dictionary<string, object> cacheDic;

        protected override void Init()
        {
            base.Init();
            //加载文件
            string fileContent = ConfigReader.GetConfigFile("ConfigMap.txt");

            configMap = new Dictionary<string, string>();

            cacheDic = new Dictionary<string, object>();

            //解析文件(string ----> prefabConfigMap)
            ConfigReader.Reader(fileContent, BuildMap);
        }

        /// <summary>
        /// 负责处理解析每行字符串的功能
        /// </summary>
        /// <param name="line">每行字符串</param>
        private void BuildMap(string line)
        {
            string[] keyValue = line.Split('=');
            configMap.Add(keyValue[0], keyValue[1]);

        }


        /// <summary>
        /// 同步加载资源
        /// </summary>
        /// <typeparam name="T">加载资源类型</typeparam>
        /// <param name="resourceName">资源名称</param>
        /// <returns></returns>
        public T Load<T>(string resourceName)where T:Object
        {
            //从字典中获取路径加载预制件
            if (configMap.ContainsKey(resourceName))
            {
                string resourceKey = $"{resourceName}_{typeof(T)}";
                if (!cacheDic.ContainsKey(resourceKey))
                {
                    T res = Resources.Load<T>(configMap[resourceName]);
                    cacheDic.Add(resourceKey, res);
                }    
                return cacheDic[resourceKey] as T;

            }
            else return default(T);
        }


        /// <summary>
        /// 异步加载资源
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="resourceName"></param>
        /// <param name="action"></param>
        public void LoadAsync<T>(string resourceName,UnityAction<T> action = null) where T : Object
        {
            StartCoroutine(LoadAsyncCore<T>(resourceName, action));
        }
        private IEnumerator LoadAsyncCore<T>(string resourceName, UnityAction<T> action) where T : Object
        {
            if (configMap.ContainsKey(resourceName))
            {
                string resourceKey = $"{resourceName}_{typeof(T)}";
                if (!cacheDic.ContainsKey(resourceKey))
                {
                    ResourceRequest request = Resources.LoadAsync<T>(configMap[resourceName]);
                    yield return request;
                    //由于采用异步协程,需要二重判断
                    if (!cacheDic.ContainsKey(resourceKey))
                        cacheDic.Add(resourceKey, request.asset as T);
                }
                action?.Invoke(cacheDic[resourceKey] as T);

            }
            else action?.Invoke(default(T));
        }

    }

}

使用方法举例如下,同步加载较为简单不再赘述

异步加载

//在资源中加载音乐文件
AudioSource bkMusic; //省略获取组件的代码
string musicName = "Bk";
ResourcesManager.Instance.LoadAsync<AudioClip>(musicName,(clip)=> {
    bkMusic.clip = clip;
    bkMusic.Play();
    bkMusic.loop = true;
    bkMusic.volume = bkVolume;
});