第05课:UI 架构案例实现
接着上一篇的内容,我们已经实现了 UI 的 View、Control、State,已经把一个 UI 窗体 Login 的基础代码写完了。在此总结一下,一个 UI 窗体主要是包含三个代码模块:Window、Ctl、State。Window 和 Ctrl 之间是通过 Event 和接口连接起来的,而 State 和 Ctrl 是通过调用接口连接起来的。具体模块之间的关系框架如下所示:
但是我们如何使用呢?下面开始介绍如何将这三者串联起来,思考一下,UI 窗体是非常多的,这么多窗体该如何管理?很容易让人想到管理类,我们需要一个窗体管理类进行统一调度,其实这种情况在开发中很常见,让人会想到工厂模式,WindowMananger 管理类具体是如何管理的?要管理这么多窗体,首先要做的事情是将其注册也就是将其存储到字典中,对应代码如下:
public enum EScenesType
{
EST_None,
EST_Login,
EST_Play,
}
public enum EWindowType
{
EWT_LoginWindow, //登录
EWT_RoleWindow, //用户
EWT_PlayWindow,//战斗
}
public WindowManager()
{
mWidowDic = new Dictionary<EWindowType, BaseWindow>();
mWidowDic[EWindowType.EWT_LoginWindow] = new LoginWindow();
mWidowDic[EWindowType.EWT_RoleWindow] = new RoleWindow();
mWidowDic[EWindowType.EWT_PlayWindow] = new PlayWindow();
}
在上述代码中,我们定义了窗体对应自己的类型,便于区分不同的窗体,构造函数实现了窗体的注册,注册好了窗体后,下面开始窗体方法的实现,比如游戏中不同的窗体之间进行 UI 切换,从一个 UI 切换到另一个 UI。再比如切换窗体到游戏场景、切换窗体返回到登录场景等等,这些方法对应的实现函数如下所示:
//切换到游戏场景
public void ChangeScenseToPlay(EScenesType front)
{
foreach (BaseWindow pWindow in mWidowDic.Values)
{
if (pWindow.GetScenseType() == EScenesType.EST_Play)
{
pWindow.Init();
if(pWindow.IsResident())
{
pWindow.PreLoad();
}
}
else if ((pWindow.GetScenseType() == EScenesType.EST_Login) && (front == EScenesType.EST_Login))
{
pWindow.Hide();
pWindow.Realse();
if (pWindow.IsResident())
{
pWindow.DelayDestory();
}
}
}
}
//切换到登录场景
public void ChangeScenseToLogin(EScenesType front)
{
foreach (BaseWindow pWindow in mWidowDic.Values)
{
if (front == EScenesType.EST_None && pWindow.GetScenseType() == EScenesType.EST_None)
{
pWindow.Init();
if (pWindow.IsResident())
{
pWindow.PreLoad();
}
}
if (pWindow.GetScenseType() == EScenesType.EST_Login)
{
pWindow.Init();
if (pWindow.IsResident())
{
pWindow.PreLoad();
}
}
else if ((pWindow.GetScenseType() == EScenesType.EST_Play) && (front == EScenesType.EST_Play))
{
pWindow.Hide();
pWindow.Realse();
if (pWindow.IsResident())
{
pWindow.DelayDestory();
}
}
}
}
上述两个方法,其实就是遍历窗体,看看它是对窗体隐藏还是显示?还是对其进行预加载操作。这两个方法会经常使用,另外,还需要提供隐藏所有窗体接口和更新窗体。代码如下所示:
public void HideAllWindow(EScenesType front)
{
foreach (var item in mWidowDic)
{
if (front == item.Value.GetScenseType())
{
Debug.Log(item.Key);
item.Value.Hide();
//item.Value.Realse();
}
}
}
public void Update(float deltaTime)
{
foreach (BaseWindow pWindow in mWidowDic.Values)
{
if (pWindow.IsVisible())
{
pWindow.Update(deltaTime);
}
}
}
因为每个 UI 窗体对应三者:Window、Ctrl、State,这样每个窗体对应的 State 跟 Window 是同样多的,而且到现在还没看到状态之间的切换,State 与 Window 是同样多的,而且这么多状态也需要一个管理类进行管理,而且我们还需要执行状态切换。将其都放到 StateManager 管理类中,这样既管理了 State,也做了状态切换处理。跟窗体设计类似,我们也需要设置状态转换类型,这个使用枚举表示:
public enum GameStateType
{
GST_Continue,
GST_Login,
GST_Role,
GST_Loading,
GST_Play,
}
接下来为了管理状态类,我们也需要将其存储在字典中,代码片段如下:
public GameStateManager()
{
gameStates = new Dictionary<GameStateType, IGameState>();
IGameState gameState;
gameState = new LoginState();
gameStates.Add(gameState.GetStateType(), gameState);
gameState = new RoleState();
gameStates.Add(gameState.GetStateType(), gameState);
gameState = new PlayState();
gameStates.Add(gameState.GetStateType(), gameState);
}
管理类主要是对于这些同类型的类模块的操作,比如若要获取到当前状态以及切换状态,游戏刚开始的默认状态,当然,还有游戏状态的更新函数 Update 或者 FixedUpdate,其实关于管理类的方法,读者在做的时候动脑子想想就有了,为了逻辑的编写,肯定需要一个统一的接口调用,否则,每个程序自己搞一套,维护起来是相当的麻烦。代码需要不停的写、不停的思考,才会有所收获,熟能生巧,状态管理类 GameStateManager 的代码如下所示:
// 获取当前状态
public IGameState GetCurState()
{
return currentState;
}
//改变状态
public void ChangeGameStateTo(GameStateType stateType)
{
if (currentState != null && currentState.GetStateType() != GameStateType.GST_Loading && currentState.GetStateType() == stateType) return;
if (gameStates.ContainsKey(stateType))
{
if (currentState != null)
{
currentState.Exit();
}
currentState = gameStates[stateType];
currentState.Enter();
}
}
//进入默认状态
public void EnterDefaultState()
{
ChangeGameStateTo(GameStateType.GST_Login);
}
public void FixedUpdate(float fixedDeltaTime)
{
if (currentState != null)
{
currentState.FixedUpdate(fixedDeltaTime);
}
}
//更新
public void Update(float fDeltaTime)
{
GameStateType nextStateType = GameStateType.GST_Continue;
if (currentState != null)
{
nextStateType = currentState.Update(fDeltaTime);
}
if (nextStateType > GameStateType.GST_Continue)
{
ChangeGameStateTo(nextStateType);
}
}
//获取状态
public IGameState getState(GameStateType type)
{
if (!gameStates.ContainsKey(type))
{
return null;
}
return gameStates[type];
}
}
上述代码,在核心函数都加了注释,读者一看就明白。本教程的最后,会把整个工程给读者,读者再结合着工程学习,印象会更加深刻。写到这里,我们的 UI 架构设计基本完整了。接下来就是开始使用了,首先建立一个类,这个类是逻辑类,需要挂接到对象上,继承 Mono,在其 Update 函数中写下下列接口调用:
//更新游戏状态机
GameStateManager.Instance.Update(Time.deltaTime);
//UI更新
WindowManager.Instance.Update(Time.deltaTime);
状态和窗体需要每帧更新,另外在类的 Start 函数中,开始进行状态切换以及进入默认状态,加入一下两行代码:
//首先是切换到登录UI
WindowManager.Instance.ChangeScenseToLogin(EScenesType.EST_None);
该函数主要是将登录UI显示出来。
//进入默认状态
GameStateManager.Instance.EnterDefaultState();
通过 EnterDefaultState 将 UI 带入到状态变换中,如果再需要切换 UI,只需要调用如下函数接口:
GameStateManager.Instance.ChangeGameStateTo(.....),
另外,WindowManager 类中的函数:
ChangeScenseToPlay 只需要切换场景时,在加载进度条调用一次即可。整个 UI 面板窗体切换都是围绕着 GameStateManger 类中的 ChangeGameStateTo 函数调用进行的。因为我们经常使用单例模式,在此把完整的代码给读者展示如下:
using UnityEngine;
using System.Collections;
namespace Game
{
public abstract class Singleton<T> where T : new()
{
private static T _instance;
static object _lock = new object();
public static T Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
_instance = new T();
}
}
return _instance;
}
}
}
public class UnitySingleton<T> : MonoBehaviour
where T : Component
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType(typeof(T)) as T;
if (_instance == null)
{
GameObject obj = new GameObject();
//obj.hideFlags = HideFlags.DontSave;
obj.hideFlags = HideFlags.HideAndDontSave;
_instance = (T)obj.AddComponent(typeof(T));
}
}
return _instance;
}
}
public virtual void Awake()
{
DontDestroyOnLoad(this.gameObject);
if (_instance == null)
{
_instance = this as T;
}
else
{
Destroy(gameObject);
}
}
}
}
游戏中的 UI 实现如下所示: