在 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);

猜你喜欢

转载自blog.csdn.net/wangjiangrong/article/details/104947494