现在游戏一般会有几种场景,例如主界面,战斗,家园等,玩家会在不同的场景之间切换。我们实现的方法可以是,始终在一个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);