在 ILRuntime 的基礎上,搭建一個簡單的UI系統(四) 場景切換

現在遊戲一般會有幾種場景,例如主界面,戰鬥,家園等,玩家會在不同的場景之間切換。我們實現的方法可以是,始終在一個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);

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章