文章目录

  • Unity实用框架(一)场景管理框架
  • 框架思路
  • IScene/ISceneManager
  • IScene
  • ISceneManager
  • UISceneManager
  • Pop
  • Push


Unity实用框架(一)场景管理框架

众所周知,Unity引擎本身提供了具有切换场景功能的SceneManager模块,但只包含比较基础的功能,比如简单的切换场景、创建场景等,想要使得我们的场景管理框架能够适用于更加复杂的情形,显然需要一套更加强大的接口。下面,笔者将提供一种撰写灵活好用的SceneManager的可行思路。

框架思路

IScene/ISceneManager

首先,为了践行面向接口编程的思想,我们需要理解我们想要达成怎样的目标,并以接口的形式将其展现出来。定义Iscene与ISceneManager以抽象场景对象和场景管理组件对象。

定义这两个接口后,在之后的实际实现中,就可以分别使新的类型实现IScene接口称为一个可加载的场景,或实现ISceneManager使得类型拥有以某种逻辑完成场景切换功能的工具(如切换UI场景的UISceneManager或切换战斗场景的BattleSceneManager)。

IScene

一个典型的场景能够定义为一组状态机,在这里,为场景定义如下状态:



unity最好用的商业框架 unity主流框架_unity最好用的商业框架

可以看到,这里将场景分为三条线,五个状态,还是比较简单的。这五个状态基本上能够满足场景管理的需求。对应上面每一个状态的,是一个函数,该函数执行状态切换时应当完成的操作。当然,由于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为例,这一套场景管理的关系如下所示

unity最好用的商业框架 unity主流框架_游戏引擎_02


更新时间:2022.7.16