场景(Scene)是Unity中组织我们的环境,物品,玩家,障碍等一切游戏相关的内容的地方。我们基本上可以把Scene当做关卡(Level)来理解。
在游戏中基本上我们不会只有一个场景,这个时候场景之间的切换就会显得尤为重要。当然,首先我们需要一个良好的设计,什么内容需要放在同一个场景下面,什么时候需要切分到不同的场景内部。把所有GameObject都放在同一场景里,显然不是一个好办法,虽然它可以极大程度地避免掉切换场景带来的消耗,但是随着场景的内容越来越复杂,可能加载当个场景就会耗费大量的时间。也就是说,每次玩家打开游戏,为了加载这个唯一的场景,可能他需要面对的是漫长的读条等待,这和主线程被阻塞没什么区别。
当然,如果将场景切分得过分细致,可能原来属于同一个关卡的内容被放到了不同的场景下面,这样带来的结果就是要频繁的切换场景。原本应该属于同一个场景的内容理论上被玩家同时访问的概率就应该会比较大,所以需要放在同一个场景下避免每次访问都要切换场景。
Unity运行中场景的加载由SceneManager来处理。在旧版本的Unity中是使用Application.LoadLevel来进行场景的加载。这个在新版本的Unity中已经升级成为了SceneManager.LoadScene。这个方法是同步地加载场景,所以如果场景比较大的话,可能会造成游戏的主线程被阻塞的感觉,LoadScene在运行的时候,我们是无法进行其他的操作的,如果场景比较小或者不需要进行其他的计算或者操作的话,可以使用LoadScene来进行加载,建议是在场景上面覆盖一个进度条来提示玩家游戏正在加载中,以免造成玩家认为游戏卡住了。
异步加载场景则是使用SceneManager.LoadSceneAsync来进行的,异步和同步地区别就在于异步加载使得游戏能够在加载场景的同时进行一些其他的运算操作。关于同步和异步的区别可以参考一下这个帖子。而Unity又提供了两种主要的加载场景模式,LoadSceneMode.Single和LoadSceneMode.Additive。
如同其字面上的意思,Single模式就是加载单个场景,意思是只会加载一个场景,其他的场景在此场景被加载之后就会被销毁。Additive模式是附加模式,新加载的场景和旧场景附加在一起,所以在场景被加载之后,旧场景不会消失,所以可以再Hierarchy Window下可以看到同时有多个场景存在。
顺带一提,异步加载场景的语法是SceneManager.LoadSceneAsync(strNameOfYourScene, LoadSceneMode.Additive)。
这样我们就可以实现批量加载场景了。如果一个主场景特别得大,我们可以将其切分成几个子场景,然后批量地加载它们,先把最基础的场景加载出来,其他的细节逐步添加进来。可能没有办法一口气全部加载出来,但是起码玩家不会感到自己被阻塞在游戏加载上面。
必须注意到的是,如果想要批量加载场景,必须将这些场景的加载模式全部设置为Additive,否则场景就会一直卡在加载状态。
在异步加载场景的过程中,我们可以将allowSceneActivation设置为false,这样可以更加稳定地来控制场景的激活。SceneManager.LoadSceneAsync返回了一个AsyncOperation, 通过这个变量,我们能够了解场景加载的进度。当AsyncOperation.progress为0.9f的时候,场景的加载完成,我们此时可以设置allowSceneActivation = true来开始激活场景。当场景完全激活,AsyncOperation.isDone变为true。
具体的实现上面,我们需要使用到协程(coroutine)。对于Unity的coroutine不太了解的话,如果你的英文够好,可以看看这一篇文章。当然百度也能找到不少关于coroutine的解释。简单来说coroutine就是类似线程的一种存在,但是它比线程更加得轻量,它能够使得程序暂停一帧的时间去执行协程,然后再返回原来的位置。注意的是,协程的返回得使用yield return ***,并且协程的返回类型是IEnumerator。
下面我们来看看实现异步加载的方法的代码的例子:
IEnumerator asynchronousLoadScene()
{
yield return null;
AsyncOperation ao = SceneManager.LoadSceneAsync(sceneName, mode);
ao.allowSceneActivation = false;
while (!ao.isDone)
{
float progress = Mathf.Clamp01(ao.progress / 0.9f);
Debug.Log("Loading progress:" + (progress * 100) + "%");
if (Mathf.Approximately(ao.progress, 0.9f))
{
Debug.Log("Almost loaded!");
ao.allowSceneActivation = true;
}
yield return null;
}
// Callback when scene is loaded
yield return StartCoroutine(OnSceneLoaded);
}
其中yield return null使得这一帧的执行结束,返回调用这个协程asynchronousLoadScene()的地方,这样能够使得游戏时间继续进行下去,而不是阻塞在一帧的时间内等待场景加载(这显然是不可能的)。
以上就是非常基础的一个场景加载的例子。如果是异步地批量加载场景,则需要用一个List数组来保存所有需要加载的场景的名字,以及加载每个场景的AsyncOperation:
IEnumerator BatchLoadingScenes(List<string> namesOfScene)
{
List<AsyncOperation> BatchAsynOperation = new List<AsyncOperation>();
for(int i =0; i < namesOfScene.Count; i++)
{
AsyncOperation SceneLoading = SceneManager.LoadSceneAsync(namesOfScene[i], LoadSceneMode.Additive);
SceneLoading.allowSceneActivation = false;
BatchAsynOperation.Add(SceneLoading);
while (BatchAsynOperation[i].progress < 0.9f)
{
yield return null;
}
}
for (int i = 0; i < BatchAsynOperation.Count; i++)
{
BatchAsynOperation[i].allowSceneActivation = true;
while (!BatchAsynOperation[i].isDone)
{
yield return null;
}
yield return StartCoroutine(OnBatchSceneLoaded[i]);
}
}
如果需要销毁场景,直接调用SceneManager.UnloadSceneAsync即可。顺带一提,所有正在加载的,已经加载的场景,都会保存在SceneManager里面,有每个场景的Index和名字,加载信息。
上面就是我这几天了解到的关于Unity中关于场景加载的一些小知识,当然还有很多坑等着我们慢慢去探索。