現在遊戲一般會有幾種場景,例如主界面,戰鬥,家園等,玩家會在不同的場景之間切換。我們實現的方法可以是,始終在一個Scene中,通過加載對應的prefab來實現。也可以是創建多個Scene,然後利用切換Scene來實現。
這裏我們使用多個Scene切換的方式來跳轉游戲場景,這樣做的好處在於Unity系統在加載新場景時,所有的內存對象都會被自動銷燬,包括你用AssetBundle.Load加載的對象和Instaniate克隆的。(不包括AssetBundle文件自身的內存鏡像,那個必須要用Unload來釋放)這樣就不需要我們手動處理大量的內存管理相關的操作,同時我們可以創建一個空場景(ClearScene)用於場景跳轉的中間場景,例如A場景跳轉到B場景,我們先從A跳轉到Clear場景,然後清理A的資源,例如Resource資源、AB資源等,清理完後再跳轉到B場景。文章參考
在場景切換過程中,和UI系統相關的主要有Loading界面的顯示,以及銷燬不需要的UI。由於代碼量比較大,所以文章中只記錄一些比較重要的部分,有興趣的小夥伴可以看看demo,一起討論。大致效果如下:
首先我們先創建兩個場景,一個空場景ClearScene用於做場景切換的橋樑,一個GameScene是我要跳轉到的目標場景,可以在裏面隨便擺放點物品,然後簡單的做個GamePanel界面,用於在GameScene中顯示。
在之前的基礎上,我們添加一個LoadingCanvas,用來顯示LoadingPanel。
namespace Hotfix.UI
{
[UI("LoadingPanel", EUIPanelDepth.Loading, true)]
public class LoadingPanel : UIPanel
{
Scrollbar m_progressBar;
Text m_progressText;
public LoadingPanel(string url) : base(url)
{
}
public override void Show()
{
base.Show();
SetProgress(0);
}
protected override void GetChild()
{
base.GetChild();
m_progressBar = transform.Find("ProgressBar").GetComponent<Scrollbar>();
m_progressText = transform.Find("ProgressText").GetComponent<Text>();
}
public void SetProgress(float value)
{
m_progressBar.size = value;
m_progressText.text = $"{(int)(value * 100)}%";
}
}
}
由於不管在哪個Scene都是必然會有UI顯示的,所以我們需要將UI節點設置爲DontDestroyOnLoad,這樣就會有一個問題,即我在第一個場景中顯示的UI會被保留到第二個場景中,但是很多UI是我在新場景中不需要的,因此我們需要在切換場景的時候需要將除了那些一直會用到的UI(例如一些對話框、提示框、菊花框等)以外的UI界面給銷燬。
我們在UIView上添加一個新的字段isDontDestroyOnLoad,若這個值爲true則在Load新場景的時候不進行銷燬,同時對UIAttribute進行了一下小修改,添加了層級以及是否銷燬的配置。
namespace Hotfix.Manager
{
public class UIAttribute : ManagerAttribute
{
public readonly EUIPanelDepth depth;
public readonly bool isDontDestroyOnLoad;
public UIAttribute(string url) : base(url)
{
depth = EUIPanelDepth.Default;
isDontDestroyOnLoad = false;
}
public UIAttribute(string url, EUIPanelDepth depth) : base(url)
{
this.depth = depth;
isDontDestroyOnLoad = false;
}
public UIAttribute(string url, EUIPanelDepth depth, bool isDontDestroyOnLoad) : base(url)
{
this.depth = depth;
this.isDontDestroyOnLoad = isDontDestroyOnLoad;
}
}
}
使用起來如下
[UI("LoadingPanel", EUIPanelDepth.Loading, true)]
接着我們在UIViewManager和UIPanelManager中添加在切換場景時調用的方法,用於銷燬
//UIViewManager
public void DestroyViewOnLoadScene()
{
for (int i = m_UIViewList.Count - 1; i >= 0 ; i--)
if(!m_UIViewList[i].isDontDestroyOnLoad)
m_UIViewList[i].Destroy();
}
//UIPanelManager
public void UnLoadPanelOnLoadScene()
{
List<string> list = new List<string>();
foreach (var panel in m_UIPanelDic.Values)
if (!panel.isDontDestroyOnLoad)
list.Add(panel.url);
foreach (var url in list)
UnLoadPanel(url);
}
剩下的就是場景切換相關的邏輯了,我們使用SceneManager.LoadSceneAsync(sceneName)方法來進行切換場景。需要注意的幾點是:
1.切換的場景需要在Unity的Build Settings的Scenes In Build中添加一下,否則會報錯
2.當返回值的allowSceneActivation設置爲false時,其progress屬性只能到0.9,並且isDone的值也不會變爲true。只有將其allowSceneActivation設爲true,isDone的值纔會變爲true。
3.allowSceneActivation設置爲true時,場景纔會切換到新場景。
加載新場景的時候,我們除了加載好Scene文件本身,還會有很多的別的需要加載,例如動態加載的人物,一些音樂文件,一些特效等等。這些都應該在我們顯示Loading界面的時候加載好。因此我們可以將上面這些分成一個個的任務LoadTask,包括場景加載(場景加載我們可以分配一個權重,及其所佔的百分比),每個任務的進度都由0到1,由自身控制。進度條的顯示爲:當前所有任務的進度之和 / 總任務數,當所有任務的進度都變爲1的時候即表明加載完成。我們新建一個SceneLoad類,用於處理場景加載。
namespace Hotfix
{
public class SceneLoad
{
//加載場景時,其他需要執行的任務。每個任務的進度爲0-1
protected delegate void LoadTaskDelegate(Action<float> callback);
protected class LoadTask
{
public float progress;
LoadTaskDelegate m_loadTask;
Action m_progressAction;
//加載任務和進度更新
public LoadTask(LoadTaskDelegate task, Action action)
{
m_loadTask = task;
m_progressAction = action;
}
public void Start()
{
progress = 0;
//執行任務
m_loadTask.Invoke((p) => {
//更新進度
progress = Mathf.Clamp01(p);
m_progressAction?.Invoke();
});
}
}
string m_sceneName;
LoadingPanel m_loadingPanel;
List<LoadTask> m_loadTaskList;//任務列表
int m_totalSceneLoadProgress;//加載場景所佔的任務數
int m_totalProgress;//總任務數(加載場景所佔的任務數+其他任務的數量,用於計算loading百分比)
bool m_isLoadFinish;
protected SceneLoad(string sceneName)
{
m_sceneName = sceneName;
m_loadTaskList = new List<LoadTask>();
RegisterAllLoadTask();
m_totalSceneLoadProgress = 1;
m_totalProgress = m_loadTaskList.Count + m_totalSceneLoadProgress;
}
public virtual void Start()
{
m_isLoadFinish = false;
m_loadingPanel = null;
UIHelper.ShowPanel<LoadingPanel>(OnLoadingPanelLoaded);
}
protected virtual void OnLoadingPanelLoaded(LoadingPanel panel)
{
m_loadingPanel = panel;
IEnumeratorTool.instance.StartCoroutine(LoadScene());
}
//註冊所有需要執行的其他任務
protected virtual void RegisterAllLoadTask()
{
}
//註冊一個新任務
protected virtual void RegisterLoadTask(LoadTaskDelegate task)
{
m_loadTaskList.Add(new LoadTask(task, UpdateLoadTaskProgress));
}
//更新任務進度
protected virtual void UpdateLoadTaskProgress()
{
float progress = m_totalSceneLoadProgress;
foreach (var task in m_loadTaskList)
progress += task.progress;
UpdateProgress(progress);
}
//加載場景前執行,主要做一些內存清理的工作
protected virtual void OnPreLoadScene()
{
UIPanelManager.instance.UnLoadPanelOnLoadScene();
UIViewManager.instance.DestroyViewOnLoadScene();
}
//更新總進度
protected virtual void UpdateProgress(float progress)
{
float progressPercent = Mathf.Clamp01(progress / m_totalProgress);
m_loadingPanel.SetProgress(progressPercent);
//所有任務進度爲1時,即加載完成
if (progress >= m_totalProgress && !m_isLoadFinish)
IEnumeratorTool.instance.StartCoroutine(LoadFinish());
}
//所有任務加載完成
IEnumerator LoadFinish()
{
Debug.Log($"Loads scene '{m_sceneName}' completed.");
OnLoadFinish();
//等待0.5s,這樣不會進度顯示100%的時候瞬間界面消失。
yield return IEnumeratorTool.instance.waitForHalfSecond;
m_isLoadFinish = true;
m_loadingPanel.Hide();
}
//加載完成時執行
protected virtual void OnLoadFinish()
{
}
//加載場景
IEnumerator LoadScene()
{
//先跳轉空場景,進行內存的清理
var clearSceneOperation = SceneManager.LoadSceneAsync(GlobalDefine.SCENE_PATH + GlobalDefine.CLEAR_SCENE_NAME);
while (!clearSceneOperation.isDone)
yield return null;
OnPreLoadScene();
GC.Collect();
Debug.Log("start load scene: " + m_sceneName);
var sceneOperation = SceneManager.LoadSceneAsync(GlobalDefine.SCENE_PATH + m_sceneName);
// When allowSceneActivation is set to false then progress is stopped at 0.9. The isDone is then maintained at false.
// When allowSceneActivation is set to true isDone can complete.
sceneOperation.allowSceneActivation = false;
while (sceneOperation.progress < 0.9f)
{
UpdateProgress(sceneOperation.progress);
yield return null;
}
UpdateProgress(1);
//爲true時,場景切換
sceneOperation.allowSceneActivation = true;
StartLoadTask();
}
//執行其他加載任務
protected virtual void StartLoadTask()
{
if(m_loadTaskList.Count == 0)
return;
foreach (var task in m_loadTaskList)
task.Start();
}
}
}
然後添加一個新的標籤SceneLoadAttribute,用於配置每個Scene的Name
public class SceneLoadAttribute : ManagerAttribute
{
public SceneLoadAttribute(string sceneName) : base(sceneName)
{
}
}
然後每個Scene都繼承於SceneLoad,例如GameSceneLoad,在子類中添加我們需要執行的額外任務
namespace Hotfix
{
[SceneLoad(GlobalDefine.GAME_SCENE_NAME)]
public class GameSceneLoad : SceneLoad
{
public GameSceneLoad(string sceneName) : base(sceneName)
{
}
protected override void RegisterAllLoadTask()
{
base.RegisterAllLoadTask();
RegisterLoadTask(LoadTask1);
RegisterLoadTask(LoadTask2);
}
void LoadTask1(Action<float> callback)
{
IEnumeratorTool.instance.StartCoroutine(Task1(callback));
}
IEnumerator Task1(Action<float> callback)
{
for (int i = 1; i < 6; i++)
{
yield return IEnumeratorTool.instance.waitForHalfSecond;
callback(0.2f * i);
}
}
void LoadTask2(Action<float> callback)
{
IEnumeratorTool.instance.StartCoroutine(Task2(callback));
}
IEnumerator Task2(Action<float> callback)
{
yield return IEnumeratorTool.instance.waitForOneSecond;
callback(0.3f);
yield return IEnumeratorTool.instance.waitForOneSecond;
callback(0.5f);
yield return IEnumeratorTool.instance.waitForOneSecond;
callback(0.8f);
yield return IEnumeratorTool.instance.waitForOneSecond;
callback(1);
}
protected override void OnLoadFinish()
{
base.OnLoadFinish();
UIHelper.ShowPanel<GamePanel>();
}
}
}
最後我們新建一個管理類SceneLoadManager,用於管理這些SceneLoad
namespace Hotfix.Manager
{
public class SceneLoadManager : ManagerBaseWithAttr<SceneLoadManager, SceneLoadAttribute>
{
Dictionary<string, SceneLoad> m_sceneLoadDic;
public override void Init()
{
base.Init();
m_sceneLoadDic = new Dictionary<string, SceneLoad>();
foreach (var data in m_atrributeDataDic.Values)
{
var attr = data.attribute as SceneLoadAttribute;
var sceneLoad = Activator.CreateInstance(data.type, new object[] { attr.value }) as SceneLoad;
m_sceneLoadDic.Add(attr.value, sceneLoad);
}
}
public void LoadScene(string scene)
{
var sceneLoad = GetSceneLoad(scene);
sceneLoad.Start();
}
SceneLoad GetSceneLoad(string scene)
{
if(!m_sceneLoadDic.TryGetValue(scene, out SceneLoad sceneLoad))
{
Debug.LogError($"[SceneLoadManager] Cannot found scene({scene}) loader");
}
return sceneLoad;
}
}
}
通過下面方法,就可以實現我們的場景切換了
SceneLoadManager.instance.LoadScene(sceneName);