unity scene 無縫切換(快取)
場景切換的時候經常會造成卡頓,導致使用者體驗並不好。
如何能做到不卡呢,有一種方式是做成prefab,然後通過啟用禁用來模擬切場景,但是現在遇到的情況是希望烘焙出的lightmap也能夠進行切換。而在unity5中動態的去切lightmap有點麻煩,沒有去深入研究,於是採用了下面快取場景的做法。
假設場景切換順序:Main->A->B->C->Main
從Main場景切到A,A載入完後希望在A執行的同時能夠快取B場景。這樣待會從B切到C時B因為已經快取過了,載入耗時會比較短,B載入同時又快取C場景,快取C的過程不會造成B的卡頓,之後會有解釋。
非同步切換場景的核心程式碼如下
LoadAsyncApi(ref _curResult,_curSceneName);
yield return StartCoroutine(LoadInProgress(_curResult));
yield return StartCoroutine(LoadAfterProgress(_curResult, _curSceneName));
private void LoadAsyncApi(ref AsyncOperation result,string sceneName)
{
result = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
}
//0-0.9
private IEnumerator LoadInProgress(AsyncOperation result)
{
result.allowSceneActivation = false;
while (result.progress < 0.9f)
{
yield return null;
}
}
//0.9-1(start awake ,not include coroutine)
private IEnumerator LoadAfterProgress(AsyncOperation result,string sceneName )
{
result.allowSceneActivation = true;
while (!SceneManager.GetSceneByName(sceneName).isLoaded)
{
yield return null;
}
}
呼叫LoadSceneAsyc非同步載入,然後將之後的載入分為兩步。
allowSceneActivation很多帖子寫的都很懵逼,今天我來講清楚。。。
1. 0-0.9(階段1)
allowSceneActivation設為false的時候,阻止Scene的完全啟用,這時進度會一直卡在0.9。這個階段會做一些磁碟相關的IO操作,比如載入資源等,這些操作不會阻塞當前主執行緒。當你的場景中引用了比較多的material啊,texture啊,那很多時間會花費在這裡。
2. 0.9-1(階段2)
將allowSceneActivation設為true,這個過程做的事情包括所有的awake,start函式的執行以及setactive的時間,應該也包括所有元件如collider,animator內部初始化相關的操作。通過實驗發現,直接在場景中加入成千上萬個立方體,最後場景載入的時間基本都花在這個階段。你手動啟用這些物體也是會造成卡頓的。這一步會造成主程序阻塞,會使得unity難以避免的卡頓,這一步暫無辦法去優化。瞭解了這兩步之後,實現這個需求就很清楚了。
Main切A載入B:
執行A的階段1,階段2,unload掉Main,再執行B的階段1,讓B卡在progress為0.9的位置。真正載入B的時候再執行B的階段2。如果說場景中沒有大量物體,而引用的資源又大又多,是可以做到秒切場景的。
提供了兩個函式,EnterScene需要提供切換到的場景和需要快取的場景。當不需要走這套機制,直接從場景中跳出到別的場景時,直接將precachescene設為null或者直接呼叫EnterOutScene退出即可。
完整程式碼:
DialogSceneMgr
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine.SceneManagement;
public class DialogSceneMgr : Singleton<DialogSceneMgr>
{
private string _curSceneName;
private string _precacheSceneName;
private bool _fromOuterScene = false;
private AsyncOperation _curResult;
private AsyncOperation _precacheResult;
void OnGUI()
{
if (GUILayout.Button("BaseLight2 cache BaseLight", GUILayout.Width(200)))
{
EnterScene("BaseLight2", "BaseLight");
//DialogSceneMgr.Instance.InitScenes(new List<string>() { "BaseLight", "BaseLight2" });
}
if (GUILayout.Button("BaseLight cache Context", GUILayout.Width(200)))
{
EnterScene("BaseLight","Context");
}
if (GUILayout.Button("SwitchOut", GUILayout.Width(200)))
{
EnterOuterScene("Context");
}
// if (GUILayout.Button("BaseLight2", GUILayout.Width(200)))
// {
// EnterScene("BaseLight2", null);
// }
// if (GUILayout.Button("BaseLight", GUILayout.Width(200)))
// {
// EnterScene("BaseLight", null);
// }
if (_curResult!=null)
GUILayout.TextArea("CurProgress:" + _curResult.progress);
if (_precacheResult != null)
GUILayout.TextArea("PrecacheProgress:" + _precacheResult.progress);
}
public void EnterScene(string sceneName,string precacheSceneName )
{
_curSceneName = sceneName;
//沒有precache的scene說明是第一次進入
_fromOuterScene = string.IsNullOrEmpty(_precacheSceneName);
//快取的場景和即將進入的場景不一致直接Load場景
if (_curSceneName != _precacheSceneName && !_fromOuterScene)
{
Clear();
SceneManager.LoadScene(_curSceneName);
return;
}
_precacheSceneName = precacheSceneName;
StartCoroutine(LoadSceneCor());
}
private void Clear()
{
_precacheSceneName = null;
}
public void EnterOuterScene(string sceneName)
{
EnterScene(sceneName, null);
}
private IEnumerator LoadSceneCor()
{
//from prev to cur
if (_fromOuterScene)
{
LoadAsyncApi(ref _curResult,_curSceneName);
yield return StartCoroutine(LoadInProgress(_curResult));
yield return StartCoroutine(LoadAfterProgress(_curResult, _curSceneName));
}
else
{
_curResult = _precacheResult;
yield return StartCoroutine(LoadAfterProgress(_curResult, _curSceneName));
}
UnLoadPrevScene();
SetActiveScene(_curSceneName);
//precache next
Debug.Log("precache next");
//沒有快取場景了,不需要快取
if (string.IsNullOrEmpty(_precacheSceneName))
{
yield break;
}
LoadAsyncApi(ref _precacheResult,_precacheSceneName);
yield return StartCoroutine(LoadInProgress(_precacheResult));
}
private void UnLoadPrevScene()
{
Scene prevScene = SceneManager.GetActiveScene();
SceneManager.UnloadScene(prevScene.name);
}
private void SetActiveScene(string sceneName)
{
SceneManager.SetActiveScene(SceneManager.GetSceneByName(sceneName));
}
//0-0.9
private IEnumerator LoadInProgress(AsyncOperation result)
{
result.allowSceneActivation = false;
while (result.progress < 0.9f)
{
yield return null;
}
}
private void LoadAsyncApi(ref AsyncOperation result,string sceneName)
{
result = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
}
//0.9-1(start awake ,not include coroutine)
private IEnumerator LoadAfterProgress(AsyncOperation result,string sceneName )
{
result.allowSceneActivation = true;
while (!SceneManager.GetSceneByName(sceneName).isLoaded)
{
yield return null;
}
}
}
附singleton
using System;
using UnityEngine;
using System.Collections;
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
private static T mInstance;
private static bool isQuiting;
public static T Instance
{
get
{
if (mInstance == null && !isQuiting)
{
mInstance = new GameObject("(Singleton) " + typeof (T).Name).AddComponent<T>();
}
return mInstance;
}
}
private void Awake()
{
T instance = this as T;
if (mInstance != null && mInstance != instance)
{
T cacheInstance = mInstance;//因為下一步destroy的時候更改了static的mInstance
DestroyImmediate(this.gameObject);
mInstance = cacheInstance;
return;
}
// 切換場景不要銷燬GameObject
DontDestroyOnLoad(gameObject);
mInstance = instance;
OnInit();
}
private void OnDestroy()
{
OnRelease();
mInstance = null;
}
private void OnApplicationQuit()
{
isQuiting = true;
}
/// <summary>
/// 初始化
/// </summary>
protected virtual void OnInit()
{
}
/// <summary>
/// 釋放
/// </summary>
protected virtual void OnRelease()
{
}
}