目录

  • 引言
  • 问题与解决过程
  • 总结


引言

读者,欢迎来到这一篇文章,你发现了吧 :) ,这一章采用了新的排版,希望你能够喜欢。
这篇文章标题叫做Resources Mng Model In Unity,即Unity中的资源管理器模块。在本文,我会介绍Unity中如何动态地加载资源,而不是像以往一样用手拖动资源。

重在尝试,放手去做吧,少年!



问题与解决过程

Q1: Unity里,如何动态加载资源?
A: 在这之前,我先告诉你一个重要文件夹,Resources文件夹,这个文件夹需要自己在项目工程中手动创建,文件夹里的内容有以下特点:

  1. 可以用ResourcesAPI加载;
  2. 打包时会压缩;
  3. 打包时还会加密;
  4. 打包后只可读,可以用ResourcesAPI加载。

简单来讲,这个文件夹的内容可以被我们用ResourcesAPI动态加载出来供我们使用。

Q2: 有哪些常用资源文件?
A: 像GameObject、AudioClip、TextAsset和Texture等都是常用的,预制体就是GameObject类型,音频文件就是AudoClip类型,文本文件就是TextAsset类型,纹理图片就是Texture类型。

Q3: 这个API怎么用?
A: 你可以查阅文档,Unity文档页面[Unity Resources内容](Unity - Scripting API: Resources)

我圈起来的就是在本文中用到的API

我主要介绍同步加载资源与异步加载资源的流程。


同步加载

步骤一般都是:

  1. Resources.Load<T>(string path)加载资源;

    用泛型重载的原因是减少装箱拆箱操作。

  2. T类型变量存储加载的资源;
  3. 使用。

其中有点特殊的是GameObject类型资源。我们需要将其加载到场景中,因此对于GameObject类型资源,应该:

  1. Resources.Load<T>(string path)加载资源;
  2. T类型变量存储加载的资源;
  3. 使用Instantiate()实例化GameObject类型资源。
1
2
3
4
5
6
//生命周期函数,在开始前一帧执行
private void Start()
{
GameObject obj = Resources.Load<GameObject>("路径");
Instantiate(obj);
}

注意:路径要求使用正斜杠/,路径名中不要包含文件的后缀,比如.prefab,.txt,可以直接填资源的名字,Unity会在子文件夹找资源。如果在Resources文件夹里有两个子文件夹,而且两个子文件夹中有同名资源,那么请保证路径名完整,不然你不知道加载的是哪个文件。


异步加载

异步加载有两种方式,直接使用API和利用协程间接异步加载

直接使用API

上图中,API返回值类型是什么?——ResourceRequest。在编辑器中按F12查看定义,

返回值里有一个asset属性

这个属性就是我们拿资源的地方。ResourceRequest可以看作一个回应,回应Resources.LoadAsync(),在这个回应里就有我们要的资源。但是这个资源是Object类型的,因此我们需要将其转换为我们需要的类型才能够使用。

ResourceRequest继承的类

里面有一个completed事件,这个事件其实就是资源加载完后执行的事件。
还有一个isDone属性,表示加载过程有没有结束。
progress属性,表示的是当前的加载进度。
代码展示如何使用这种方式进行异步加载资源(非泛型版本):

1
2
3
4
5
6
7
8
9
10
//生命周期函数,在开始前一帧执行
private void Start()
{
//非泛型版本
ResourceRequest request = Resources.LoadAsync("路径");
//假设目标类型是GameObject类型
GameObject obj = request.asset as GameObject;
//别忘了GameObject类型有点特殊,需要实例化到场景中
GameObject.Instantiate(obj);
}

代码展示如何使用这种方式进行异步加载资源(泛型版本):

1
2
3
4
5
6
7
8
9
10
//生命周期函数,在开始前一帧执行
private void Start()
{
//泛型版本
ResourceRequest request = Resources.LoadAsync<GameObject>("路径");
//假设目标类型是GameObject类型,非泛型版本无需进行类型转换,已经在内部处理好了
GameObject obj = request.asset;
//别忘了GameObject类型有点特殊,需要实例化到场景中
GameObject.Instantiate(obj);
}

插入:Q4: 为啥要用异步加载?
A:同步加载都会在一帧里执行加载逻辑,然后当前帧就能获得资源,但是如果要加载的资源特别大呢?这时候资源要加载的时间就变长了,如果你是帧数很高,说明每一帧的时间间隔很短,这样一来这一帧做不完的事情就要用到很多帧,就会造成卡顿。因此使用异步加载的方式,缓解卡顿,但是不知道何时能够获得资源。
Q5:怎么知道已经获得资源并可以使用它了呢?
A:读者,还记得介绍的isDone属性吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//了解何时获得了资源
//生命周期函数,在开始前一帧执行
private void Start()
{
ResourceRequest request = Resources.LoadAsync<GameObject>("路径");
while(!request.isDone)
{
//只要没完成就在这里
//还记得progress吗
//可以用这个实时显示加载进度
Debug.Log("当前加载进度为:{0}", request.progress);
}
GameObject obj = request.asset;
GameObject.Instantiate(obj);
}

协程间接异步加载资源

这种方式更常用、更灵活。可以在加载资源完毕后做一些事情。我在这里默认读者你已经接触过协程相关内容。
过程:

  1. 首先声明一个协程函数;
  2. 在协程函数中异步加载资源;
  3. 外部开启协程。

代码如下:

1
2
3
4
5
6
7
//协程函数
private IEnumerator LoadRes(string path)
{
//假设我要加载的是一个GameObject类型对象
ResourceRequest request = Resources.LoadAsync<GameObject>(name);
yield return request;
}

yield return request!Q6: 为什么能够这样使用?
A: 哈哈哈,读者,请往上看图片,你记得吗?ResourceRequest的父类是AsyncOperationAsyncOperation的父类是YieldInstruction,没错,正是YieldInstruction类,yield return语句后接上一个继承自该类的对象,会告诉Unity协程函数什么时候继续执行代码。在这里,Unity协程函数会在request已经加载完后继续执行下面的代码。

Q7: 我怎么操作对象呢?
A: 听说过回调函数吗,用这个把资源传出去操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//协程函数
private IEnumerator LoadRes<T>(string path, UnityAction<T> callback)
{
//假设我要加载的是一个GameObject类型对象
ResourceRequest request = Resources.LoadAsync<GameObject>(name);
yield return request;
callback(request);
}

//生命周期函数,在开始前一帧执行
private void Start()
{
StartCoroutine(LoadRes<GameObject>("路径",(GameObject obj)=>{
//对参数做一些事情
GameObject.Instantiate<GameObject>(obj);
}));
}

看到这里,读者你已经明白了如何使用Resources中的API加载资源了。接下来我将介绍一个资源加载模块,提供给外部加载资源。

分析一下资源加载模块需要具备的功能:

  1. 同步加载资源
  2. 异步加载资源

OK,接下来我将逐步完善一个资源加载模块,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//资源加载模块:BaseMng<T>是之前实现的单例模式基类
public class ResMng : BaseMng<ResMng>
{
//同步加载资源
public T Load<T>(string path)
{
T res = Resources.Load<T>(path);
//因为GameObject类型资源需要实例化到场景中
if(res is GameObject)
{
//这里需要注意,我们创建了一个副本引用实例化的资源
GameObject obj = Instantiate(res as GameObject);
return obj as T;
}
}
}

Q8: 为什么创建了一个副本引用实例化的资源呢?
A: 读者,我很高兴能为你解释这个问题,我在开发过程中遇到了一个问题,和这个相关。我用资源加载模块加载了一个预制体对象,我通过回调函数把这个对象给外部使用,我在外部修改这个对象的时候,报错了——我修改的是Transform组件,给这个预制体对象一个父节点,报错显示“为了避免数据冲突,在预制体中的Transform组件不能被设置”。
重点在 Resources.Load<GameObject>()Resources.LoadAsync<GameObject>(),前者返回的是预制体对象 ,后者返回的 asset中存的也是预制体对象,我们修改相当于修改预制体了,所以我们要创建一个副本,返回的也是这个副本。
意思就是我们修改的是实例化后的副本,而不是预制体本身(会有冲突)。

好的,我们继续完善异步加载资源的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//资源加载模块:BaseMng<T>是之前实现的单例模式基类
public class ResMng : BaseMng<ResMng>
{
//同步加载资源
......

//异步加载资源
//callback是回调函数,把加载好的资源给外部处理的
//之所以不需要返回值,因为资源并不一定是在这一帧返回的,这归于异步加载,这也是我们使用回调函数交给外部处理资源的原因
public void LoadAsync<T>(string path, UnityAction<T> callback)
{
//由于我们这个类没有继承MonoBehaviour,需要使用到公共Mono模块
MonoMng.GetInstance().StartCoroutine(path,callback);
}
//协程函数
private IEnumerator RLoadAsync<T>(string path, UnityAction<T> callback)
{
ResourceRequest request = Resources.LoadAsync<T>(path);
yield return request;

if(request.asset is GameObject)
{
GameObject obj = GameObject.Instantiate(request.asset as GameObject);
callback(obj as T);
}
else
{
callback(obj as T);
}
}

}

Q9: 异步加载资源时,如果是一个预制体GameObject,我可以给他一个名字吗,方便找?
A: 当然可以,这是一个明智的决定。你只需要在代码中添加:

1
obj.name = path;

或者你可以制定一套自己的命名规则。
这样一来,我们的资源管理模块就完成了:)



总结

资源管理模块的功能是为了封装好API,供外部使用,快速加载资源,减少一些重复的工作,如实例化预制体,绑定音频文件等。
资源管理模块具备:

  1. 同步加载资源,加载较小的资源;
  2. 异步加载资源,加载较大的资源。

要注意的坑:

  1. 加载预制体时,需要创建一个GameObject副本存储实例化的对象,对这个副本进行修改,不然会修改预制体;
  2. 回调函数的使用,因为异步加载资源不会在同一帧就把资源交给我们,所以需要等资源加载完毕后交给外部处理,就使用了回调函数。