本文分享Unity中的资源管理-一整套简单的资源管理方案(1)
经过了一系列的文章, 我们终于来到了本专题的最后一部分.
在接下来的几篇文章中, 作者会提供一套简单的资源管理方案给大家学习和参考, 相信只要简单的修改就可以直接在项目中使用.
整套方案基本会涵盖我们之前文章提到的所有内容.
在本篇文章中, 我们先提纲挈领的将核心的结构和布局作简单介绍, 在后续的文章中将其填充和丰满.
好了, 废话不多说, 马上开始今天的内容.
需求分析
首先我们需要在Runtime下使用Ab
或者Resources
, 在Editor下使用AssetDatabase
或者Resources
.
然后, 我们需要根据文件名+扩展名的方式使用资源, 这要求资源不能重名, 且需要一个资源与路径的映射关系管理.
最后我们需要分别针对需要实例化的资源和不需要实例化的资源作不同的管理. 这通过对象池和引用计数来实现.
资源目录结构设计
我们使用公用加系统, 然后分别进行分类的方案, 即:
- 资源分类:
-
Textures
: 纹理 -
Sprites
: 精灵 -
Prefabs
: 预制 -
Shaders
: 着色器 -
Materials
: 材质 -
AnimationClips
: 动画片段 -
Animators
: 动画控制器 -
Spines
: 骨骼 -
Font
: 字体 -
AudioClip
: 声音片段 -
Config
: 配置文件 -
BehaviorTree
: 行为树 -
Models
: 模型 -
Scenes
: 场景
-
-
Config
: 保存一些资源配置信息 -
Common
: 公共目录, 包含所有资源类型 -
Systems
: 系统目录, 分各个系统的资源目录, 主要是Textures/Sprites/Prefabs
我们大概是将公用部分的每个文件夹设计为一个Ab
包, 如果某个类型的包内容过多, 如AnimationClips
, 或者可以划分为单独的个体, 如Models
中, 每个模型所有的资源可以组成一个单独的文件夹, 这些情况下可以进一步划分为单独的Ab
包.
公用的资源一般不主动卸载, 只在场景切换, 或者内存达到警戒值时手动触发无用资源(Resources.UnloadUnusedAssets
)进行清理.
然后系统中的每个系统为一个Ab
, 这样能够在不使用某个系统时单独卸载. 当然, 为了提高用户体验, 也可以选择不立即卸载, 而是采用一些其它的优化策略, 本实例为了简单, 采用直接卸载的方式.
如果某个系统比较庞大, 如战斗系统, 则可以进一步划分, 总体按照系统到子系统的模式.
同时为了简化示例, 我们直接使用Asset Labels
来管理Ab
, 而不通过代码管理的方式, 有兴趣的同学可以参考前面的文章.
整个目录结构大概是这样的:
角色设计
正常情况下, 用户(业务开发者)其实不关心我们使用的到底是Ab
还是AssetDatabase
, 亦或是Resources
.
所以我们第一个角色就是: AssetManager
.
AssetManager的主要职责与主要接口
我们使用AssetManager
来担任整个资源管理对外的窗口, 用户只需要调用它的接口取用和归还资源即可.
也就是说AssetManager
会提供取用和归还资源的接口, 同时负责在内部维护资源的引用关系, 在合适的时机加载和卸载资源或者销毁实例化对象.
因为很多时候, 我们会同时请求多个资源, 如果让用户自己做记录, 然后在不使用的时候进行归还, 那么会增加用户的使用成本, 所以我们需要一个角色来承担这份工作, 这是我们第二个角色: AssetLoader
.
AssetLoader的主要职责与接口
我们使用AssetLoader
来承担用户与AssetManager
之间的窗口, 用户向AssetManager
请求新的AssetLoader
, 并实际使用AssetLoader
来请求资源和归还资源. 使用完毕后一次性销毁AssetLoader
即可完成所有资源的归还和销毁.
常见的使用的场景是每个窗口打开时请求新的AssetLoader
, 后续所有的预制, 特效, 纹理等资源, 都通过该AssetLoader
请求, 在退出窗口时, 销毁该AssetLoader
即可. 当然, 可以使用对象池技术来降低AssetLoader
实际新建和销毁频率, 使用引用计数来确保资源的正常卸载.
这样也可以结合窗口或者显示层管理器, 将这部分封装到框架层, 在合适的生命周期中调用, 让用户不操心这个过程.
用户实际拿到的是资源本身, 或者资源的实例化对象, 后面隐藏的所有的机制都不需要关心.
资源信息相关类
为了维护资源的使用状态等信息, 我们需要将资源划分为不同的信息角色:
-
资源本身的信息,
AssetInfo
, 支持引用计数, 核心属性为:- 名称:
string assetName
- 全路径:
string fullPath
- 所在
Ab
包信息:AbInfo abInfo
- 资源实体的引用:
asset
- 依赖的资源信息列表:
List<AssetInfo> dependencies
- 名称:
-
资源所在
Ab
信息,AbInfo
, 支持引用计数, 核心属性为:- 名称:
abName
- 全路径:
fullPath
-
Ab
实体的引用:ab
- 依赖的其它
Ab
信息列表:List<AbInfo> dependencies
- 名称:
-
资源的实例化对象信息,
ObjInfo
, 支持引用计数, 核心属性为:- 资源信息的引用:
AssetInfo assetInfo
- 资源信息的引用:
-
这里顺便说一下, 如果使用代码添加
Ab
的方式, 可以加入下面的角色来序列化资源和路径的映射关系, 我们的方案里没有使用, 但是还是给大家列出来-
资源本身的配置信息,
AssetConfigInfo
, 支持序列化, 用于映射资源名与路径或者Ab
信息- 名称:
string assetName
- 全路径:
string fullPath
- 所在
Ab
包名:string abName
- 名称:
-
所有资源的配置信息,
AssetConfigInfoAsset
, 支持序列化, 用于存放所有的AssetConfigInfo
-
这里是简单的代码:
-
[Serializable]
public class AssetConfigInfo {
public string assetName;
public string abName;
[NonSerialized]
public string fullPath;
}
public class AssetConfigInfoAsset : ScriptableObject {
[SerializeField]
public List<AssetConfigInfo> assetLst;
[NonSerialized]
public Dictionary<string, AssetConfigInfo> assetDict;
public void Mapping() {
Assert.IsNotNull(assetLst, "资源列表为空!");
if (assetDict != null) return;
assetDict = new Dictionary<string, AssetConfigInfo>(assetLst.Count);
foreach(var assetConfigInfo in assetLst) {
if (assetDict.ContainsKey(assetConfigInfo.assetName)) {
Assert.IsTrue(false, string.Format("资源重名!: assetName {0}; abName {1}; fullPath {2}, ", assetConfigInfo.assetName, assetConfigInfo.abName, assetConfigInfo.fullPath));
}
assetDict.Add(assetConfigInfo.abName, assetConfigInfo);
}
}
}
目前暂时只需要这些内容和角色, 如果有需要会在后面的文章中添加.
今天没有太多内容, 主要还是设计思想. 我们将从下篇文章开始将设计方案进行实施. 感兴趣的同学可以持续关注, 希望对大家有所帮助.