文章目录
- Unity实用框架(一)场景管理框架
- 框架思路
- IScene/ISceneManager
- IScene
- ISceneManager
- UISceneManager
- Pop
- Push
Unity实用框架(一)场景管理框架
众所周知,Unity引擎本身提供了具有切换场景功能的SceneManager模块,但只包含比较基础的功能,比如简单的切换场景、创建场景等,想要使得我们的场景管理框架能够适用于更加复杂的情形,显然需要一套更加强大的接口。下面,笔者将提供一种撰写灵活好用的SceneManager的可行思路。
框架思路
IScene/ISceneManager
首先,为了践行面向接口编程的思想,我们需要理解我们想要达成怎样的目标,并以接口的形式将其展现出来。定义Iscene与ISceneManager以抽象场景对象和场景管理组件对象。
定义这两个接口后,在之后的实际实现中,就可以分别使新的类型实现IScene接口称为一个可加载的场景,或实现ISceneManager使得类型拥有以某种逻辑完成场景切换功能的工具(如切换UI场景的UISceneManager或切换战斗场景的BattleSceneManager)。
IScene
一个典型的场景能够定义为一组状态机,在这里,为场景定义如下状态:
可以看到,这里将场景分为三条线,五个状态,还是比较简单的。这五个状态基本上能够满足场景管理的需求。对应上面每一个状态的,是一个函数,该函数执行状态切换时应当完成的操作。当然,由于load状态要负责场景资源的加载,我们自然而然地将其返回值定义为IEnumerator以应用协程。
IEnumerator Load(object savedState);
此外,在接口中定义两个属性,一个为name(string)以标识场景,另一个为ISceneManager接口,此成员应当作为当前SceneManager单例的引用,用于完成Scene与SceneManager间的交互。
IScene的完整定义如下:
public interface IScene
{
string Name { get; }
ISceneManager SceneManager { get; set; }
IEnumerator Load(object savedState);
void Begin();
void Resume();
void Pause();
void Finish();
}
ISceneManager
在这里,我们在SceneManager使用栈结构来储存场景。当然这并非编写场景管理框架的唯一解决方案,但使用栈自然有它的好处所在。进入下一个页面/回到上一个页面是场景切换中经常出现的模式,使用栈结构能很好的适应这种情况。
既然是使用栈,那么ISceneManager自然就应当定义Push和Pop两种状态,当然,为了更加灵活地处理场景切换,添加replace状态,该状态相当于pop和push的组合,但省去了中间状态。
同时,对于场景切换的中间态,我们单独用一个类来定义它:
public class Transition
这个“中间态”类将极大地扩展我们的SceneManager的可用性,它包含了前一个场景、后一个场景,以及场景切换的动态效果,此类并非实际实现,想要实现类似渐入渐出等效果,应当撰写此类的子类。
public enum TransitionType
{
Push,
Pop,
Replace
}
public class Transition
{
public TransitionType TransitionType { get; set; } //转换类型
public IScene NextScene { get; set; } //下一个场景
public Type NextSceneType { get; set; } //下一个场景的类型
public object SavedState { get; set; } //保存的场景数据
public event Action TransitionEnded; // 回调事件
public event Action TransitionFadeOuted; // 回调事件
internal void RaiseTransitionEnded()
{
TransitionEnded?.Invoke();
}
internal void RaiseTransitionFadeOuted()
{
TransitionFadeOuted?.Invoke();
}
}
最后,在ISceneManager接口中,包含了当前正在运行的场景、当前指定的Transition,切换场景的方法和回调事件。
public interface ISceneManager
{
IScene CurrentScene { get; }
Transition CurrentTransition { get; }
event Action<Transition> TransitionStarted;
event Action<Transition> TransitionEnded;
void SetTransition(Transition transition);//可理解为原生SceneManager的loadscene函数。
}
UISceneManager
现在,我们来完成IsceneManager的一个实际实现。不同种类的场景可能需要应用不同的SceneManager(都实现ISceneManager接口),这里以UI为例。
首先,是UI界面需要的一些组件:
private UIRoot uiRoot; //NGUI相关,如果使用UGUI或者其他GUI控件就不需要它
private UICamera uiCamera; //拍摄UI界面的摄像机
private LayerMask eventReceiverMask = -1; //层级遮罩
然后,需要一个栈储存当前加载了的所有场景
private List<IUIOverlay> stackedOverlayList = new List<IUIOverlay>();
private List<KeyValuePair<IScene, object>> stackedScenes = new List<KeyValuePair<IScene, object>>();
其中,上面的IUIOverlay接口定义了当前UI界面上的其他UI,或者说画中画,IUIOverlay本身不在本文的讨论之列。列在这里的目的在于,读者可以清楚如果想用SceneManager实现画中画、弹窗等效果,可以让弹窗、画中画的控制脚本实现此接口,以方便在SceneManager中统一管理。
当然,我们的重点在于SetTransition函数实现。
public void SetTransition(Transition transition)
{
if (CurrentTransition != null)
{
return;
}
var t = transition as UITransition; //UITransition是Transition的子类
if (t.Animation == TransitionAnimation.None)
{
CoroutineManager.Instance.StartCoroutine(UIRoot, SetTransitionAsync(transition));
}
else if (t.Animation == TransitionAnimation.Fade)
{
CoroutineManager.Instance.StartCoroutine(UIRoot, SetFadeTransitionAsync(transition));
}
else
{
CoroutineManager.Instance.StartCoroutine(UIRoot, SetSlideTransitionAsync(transition));
}
}
不同的场景转换效果(None无效果,Fade渐入渐出,Slide幻灯片式滑出,这里假设只有这三种方式)需要不同的转换函数,因此转交给不同的函数进行调用。注意场景转换这种开销明显较大的操作应当应用协程。这里的CoroutineManager也并非原生的协程管理器,在其它文章中,笔者将会介绍一个优雅的CoroutineManager将如何设计。
场景转换时要做些什么呢?首先,需要判断当前的转换方式时Push还是Pop,以此判断是从stackedScene中取出场景还是压入场景。
Pop
对于pop,所要做的就是弹出当前场景,并从存于栈中的场景中取出一个并加载。
if (transition.TransitionType == TransitionType.Pop)
{
if (stackedScenes.Count > 0)
{
var kv = stackedScenes[stackedScenes.Count - 1];
transition.NextScene = kv.Key;
transition.SavedState = kv.Value;
stackedScenes.RemoveAt(stackedScenes.Count - 1);
}
}
CurrentTransition = transition;
//场景类和其数据类共同组成了transition
var nextScene = transition.NextScene;
var savedState = transition.SavedState;
//别忘了在Iscene中定义的状态!在取出栈中场景后,销毁当前场景前,先调用pause
currentScene?.Pause();
Push
对于push,要将当前场景和需要保存的场景数据压入栈。
if (transition.TransitionType == TransitionType.Push && currentScene != null)
{
stackedScenes.Add(new KeyValuePair<IScene, object>(currentScene, nullSavedState));
}
currentScene?.Pause();
压入栈中时,随场景一起压入的是一个空数据,此数据将在场景建立完成后再被填充。由于一般来说,我们需要保存场景在被销毁前的最后一个状态,因此,最合理的方式应该是在IScene.Finish()函数中将数据存入(还记得上面的IScene状态机吗),关于数据的形式不会再本篇文章中阐述。显然,对于每个场景,需要保存的数据种类相差甚远,因此对于每个继承自IScene的场景类,编写一个对于该场景的data类是合理的。比如,对于游戏大厅场景LobbyScene :IScene,写一个LobbySceneData来存储它的数据。这个类就是IScene在加载场景时,load函数所获取的参数!
IEnumerator Load(object savedState);
在导入新场景之后,就可以调用当前场景的finish函数来销毁它,并且加载现在的场景了。编写符合需求的函数来完成场景转换的动画,比如用遮罩来实现渐入渐出、或者canvas上的"loading"字样,不在这里赘述。
//统一场景转换动画?
currentScene?.Finish();
//统一场景转换动画?
currentScene = nextScene;
StartCoroutine(currentScene.Load(currentScene.data));
//...
currentScene?.Begin();
//..
currentScene?.Resume();
//..
综上,我们就实现了一个最简单的场景管理器!以LobbyScene为例,这一套场景管理的关系如下所示
更新时间:2022.7.16