場景(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中關于場景加載的一些小知識,當然還有很多坑等著我們慢慢去探索。