Unity Resources资源管理器
文章目录
- Unity Resources资源管理器
- 一、目的
- 二、Resources管理器
- 1. 路径问题的解决
- 2. 文件的读取和解析
- 3. ResourcesManager
一、目的
通常来讲,直接从Resources下进行资源的加载,有几个明显的缺点
- 需要指明资源的详细路径,一旦资源位置迁移,代码也要做出相应的改变
- 缺少缓存机制,从Resources下加载资源的过程较为耗时,同一个资源只加载一次
- 部分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;
}
}
注意点
- Unity提供的AssetDatabase.FindAssets方法,指定的类型无视大小写,但类型名必须正确,图片texture , 音乐audioclip, 预制体prefab等等。
- 最终生成的路径映射和名称,均不带后缀,这也是为什么要添加filter List的原因,应该把此种类型文件的后缀都删除。
- 如果除了图片,音乐,预制体有新的资源存储,应在代码的TODO处添加对应新的类型和筛选器列表。
最终能在StreamingAssets文件夹下得到一个ConfigMap.txt如下图样式
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;
});