为了节省内存,游戏的一些资源往往需要在运行时(runtime)动态加载。如果资源本身加载比较耗时,采用同步方法会产生卡顿现象,对此的解决方法通常采用多线程或者使用引擎本身自带的异步加载方法。在Unity开发中,由于一些方法(如Resources.Load)本身不支持在其它线程调用,因此多线程的使用会受到限制;而Unity脚本API对许多加载方式都有相应的异步方法,因此我们需要对Unity异步加载方法的机制有一定的理解。此外,协同进程(Coroutine)常常和异步加载方法联合使用,这里顺便也研究一下。
1. Unity异步加载资源
Unity在runtime加载资源有Resources和AssetBundle两类方法,由于AssetBundle需要先进行打包,因此在Editor中进行开发测试阶段使用不太方便。这里介绍Resources.LoadAsync异步加载资源方法,先上代码和运行结果:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class tryLoadAsync : MonoBehaviour
{
public static TerrainData td;
public static GameObject terrain00;
private ResourceRequest request;
void Start()
{
#if UNITY_EDITOR
request = Resources.LoadAsync("tt/myTD00"); //调用异步加载方法
Debug.Log("异步方法!");
#endif
}
void Update()
{
if (request != null)
{
if (request.isDone)
{
Debug.Log("异步加载完成了!");
td = request.asset as TerrainData;
if (!td)
{
Debug.Log("加载terraindata失败!");
}
//创建地形
terrain00 = Terrain.CreateTerrainGameObject(td);
terrain00.transform.position = new Vector3(0, 0, 0);
terrain00.GetComponent<Terrain>().Flush();
request = null;
}
else
{
Debug.Log("已经过了一帧了!"); //异步方法至少需要一帧的时间才能生效!
}
}
}
}
异步加载和同步加载的区别用另外一种概念描述就是“阻塞”。同步方法会阻塞在当前代码的执行,而其它部分(如UI)都在等着它结束调用,因此如果资源加载很耗时,那么就会出现“卡住了”的现象。异步加载则是非阻塞的,调用完异步方法后,代码继续执行,而加载工作由Unity在后台另开辟一个异步线程来进行。比如上面的代码中,在初始化Start方法中调用了Resources.LoadAsync异步方法加载一个TerrainData地形资源,该方法立即返回一个ResourceRequest类型的消息,之后继续执行下一条Debug.Log语句,而并不会等待加载的完成。
加载状态的查询有两种方法:一种是后面要介绍的yield return方式,另一种是这里采用的手动查询方式。我们在每帧运行的Update方法中通过ResourceRequest的isDone成员查询异步加载是否完成了。但是从控制台窗口的输出来看,第一帧执行的是else分支,也就是在第一帧时异步加载还没有完成。这并不是因为我们的资源过大(事实上这个TerrainData资源也很小),而是异步方法的生效至少要一帧的时间。异步加载的这个特性是十分重要的,不仅对于Resources类的方法,对于AssetBundle也一样。比如,你要在Update中查询一些游戏状态以决定是否要加载新的资源,你编写了判断条件、加载代码,这都很很棒。然后你继续编写代码想让这些资源立即生效,然而抱歉,在这一帧你的这些代码就没有任何意义了,因为无论你怎么样查询加载状态,它都会告诉你异步加载还没有成功,因为它至少有一帧的延迟
这种情况是值得注意的。
2. Couroutine的使用
协同进程Coroutine并不是线程,它使用yield return语句强行将代码分为两部分执行,每一帧Unity都会查询一下yield返回之前的位置是否满足条件。如果满足那么继续运行协程剩余的代码,并且至少会推迟一帧。比如下面的代码和运行结果:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
public class tryCoroutine : MonoBehaviour
{
public static TerrainData td;
public static GameObject terrain00;
private ResourceRequest request;
// Use this for initialization
void Start()
{
#if UNITY_EDITOR
StartCoroutine(LoadTerrain());
Debug.Log("开始协同进程!");
#endif
}
IEnumerator LoadTerrain()
{
request = Resources.LoadAsync("tt/myTD00");
yield return request;
Debug.Log("异步加载完成了!");
td = request.asset as TerrainData;
if (!td)
{
Debug.Log("加载terraindata失败!");
}
terrain00 = Terrain.CreateTerrainGameObject(td);
terrain00.transform.position = new Vector3(0, 0, 0);
terrain00.GetComponent<Terrain>().Flush();
}
// Update is called once per frame
void Update()
{
if (td == null)
Debug.Log("等一等");
}
}
程序实现的功能和上面一样,都是加载一个TerrainData资源并创建一个地形。该脚本创建了一个名为LoadTerrain的Coroutine,注意返回值一定是IEnumerator类型。在第一次运行至yield return语句时,程序返回,执行该帧的Update方法;在下一帧,由于异步加载完成,程序回到yield return的位置,继续执行这个Coroutine剩余的代码。可见,Coroutine并不是一个线程,它仍然运行在主线程中。
参考文献:
1. AssetBundle官方教程:https://unity3d.com/cn/learn/tutorials/topics/best-practices/guide-assetbundles-and-resources
2. Unity Script API:https://docs.unity3d.com/ScriptReference/index.html