Eevee框架10——UI框架
想要一个完美的UI框架,貌似太理想了,这里参考了唐老师、siki等能百度到的很多UI框架,发现大家都只是做了界面的加载、调用、关闭等处理,类似于一个针对UI的缓存池子,区别就是增加了一些开关刷新等事件的监听。
综合参考了多方之后最后决定使用TTUIFramework。
因为其他UI框架可能更加偏向于一些简易的通用性游戏界面制作,跟本人方向并不太符合,而TTUIFramework则以Page的形式定义一个个页面,我觉得是一种很好的想法,从本质上每个页面并无区别,也可以从任何一个页面跳转到任何一个页面,根据其设定可以按照设定返回。
先说一下TTUIFramework的核心:其实就是TTUIPage脚本,另外俩一个是UIRoot动态创建,一个是绑定加载逻辑,都可以忽略。而在TTUIPage使用一个静态List管理Page,在定义的时候设定界面类型(个人觉得无法涵盖所有类型),在打开界面或者关闭界面时再根据这个类型去遍历处理其他页面是否打开。
优缺点:优点其实上面都说的差不多了,说说缺点吧:
1.一层一层返回的需求其实我遇到的大部分项目并不能用到,更复杂的界面跳转也不建议放在架构里做
2.UI遮挡层级的管理比较简陋
3.仅支持单Canvas
根据TTUIFramework(以下简称TT)的源码个人做出了以下几点改变:
1.TTU中主要增加了一个List,用来存储页面层级,然后按照层级一级一级地去关闭(并且把该功能作为了主逻辑),个人做过的项目中,貌似还用不到这个需求:即有一个显示在最高层的返回按钮,点一下执行一下返回上个界面(返回按钮就不需要关心当前在哪个界面了)。如果有类似需求的小伙伴可以参考TT的源码,我这里直接去掉了这部分管理。
2.因为经常做展示类项目,就会涉及到一个录屏的问题,录屏时经常会需要有些UI录制到视频里,有些不录制,就需要多个Canvas而非仅在一个Canvas下展示,因此我移除了TT原来的Canvas系统。新写了一套简单的Canvas,主要用于挂载物体时找到对应的层级位置
3.增加了不同层级用于管理显示的前后关系
目前位置该套架构仅按照TT的demo修改了一下,未实际使用(可能会有其他问题),不过还是先给个初版出来,后续在实际使用过程中看看是否有Bug或优化点。
不想看代码的直接看demo吧:
链接: https://pan.baidu.com/s/13WgqA5bGinLzw9jwxsqv-g
提取码: 55x5
注释写的很全,但还是稍微讲一下代码:
还是先看如何使用:
我创建了默认的三个Canvas,如果不需要可以自行关闭或者删除:
image.png
同样的,UIRoot的脚本不需要也可以删除,需要更多的也可以Control CV复制出来
image.png
Canvas相关的逻辑在这三个继承UIRoot的子类里修改即可。普通项目可能只需要一个Canvas就行,增加Canvas的时候记得调节子物体相机的渲染层级。
先看一下UIRoot父类,其实就加了一些物体,增加了添加代码自动绑定参数的功能,用于UIPage设置对应的父物体:
using System.Collections;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine;
/// <summary>
/// UI Root
/// </summary>
public abstract class UIRoot : MonoBehaviour
{
protected static UIRoot instance = null;
public static UIRoot Instance
{
get
{
return instance;
}
}
[SerializeField]
private Transform m_BackGround;
/// <summary>
/// 背景图
/// </summary>
public Transform BackGround
{
get { return m_BackGround; }
}
[SerializeField]
private Transform m_MainMenu;
/// <summary>
/// 主界面
/// </summary>
public Transform MainMenu
{
get { return m_MainMenu; }
}
[SerializeField]
private Transform m_SecondaryMenu;
/// <summary>
/// 二级界面
/// </summary>
public Transform SecondaryMenu
{
get { return m_SecondaryMenu; }
}
[SerializeField]
private Transform m_ThirdMenu;
/// <summary>
/// 三级界面
/// </summary>
public Transform ThirdMenu
{
get { return m_ThirdMenu; }
}
[SerializeField]
private Transform m_Popup;
/// <summary>
/// 普通弹出框
/// </summary>
public Transform Popup
{
get { return m_Popup; }
}
[SerializeField]
private Transform m_AlwaysFront;
/// <summary>
/// 提示类弹出框
/// </summary>
public Transform AlwaysFront
{
get { return m_AlwaysFront; }
}
[ContextMenu("自动添加绑定关系")]
void SetParameters()
{
m_BackGround = transform.Find("BackGroundRoot背景图");
m_MainMenu = transform.Find("MainMenuRoot主界面");
m_SecondaryMenu = transform.Find("SecondaryMenuRoot二级界面");
m_ThirdMenu = transform.Find("ThirdMenuRoot三级界面");
m_Popup = transform.Find("PopupRoot普通弹出框");
m_AlwaysFront = transform.Find("AlwaysFrontRoot提示类弹出框");
}
void Reset()
{
Debug.Log("脚本自动添加绑定关系");
SetParameters();
}
}
然后我们看一下Page怎么继承,需要重写一下构造函数,定义一下三个参数类型(绑定的UIRoot,设置层级,碰撞设置):
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class UINotice : UIPage
{
public UINotice() : base(ButtomUIRoot.Instance, UIHierarchy.Popup, UICollider.Normal)
{
m_UIPath = "UIPrefab/Notice";
}
public override void Awake(GameObject go)
{
this.m_GameObject.transform.Find("content/btn_confim").GetComponent<Button>().onClick.AddListener(() =>
{
Hide();
});
}
public override void Refresh()
{
}
}
最后是UIPage的基类:
using System;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Object = UnityEngine.Object;
#region define
/// <summary>
/// UI层级,根据不同层级设置父物体,如果展示层不够可自行扩展,需要创建对应的物体并调节相对位置
/// </summary>
public enum UIHierarchy
{
/// <summary>
/// 背景图
/// </summary>
BackGround,
/// <summary>
/// 主界面
/// </summary>
MainMenu,
/// <summary>
/// 二级界面
/// </summary>
SecondaryMenu,
/// <summary>
/// 三级界面
/// </summary>
ThirdMenu,
/// <summary>
/// 普通弹出框
/// </summary>
Popup,
/// <summary>
/// 提示类弹出框
/// </summary>
AlwaysFront
}
/// <summary>
/// UICollider碰撞遮挡设置
/// </summary>
public enum UICollider
{
/// <summary>
/// 显示该界面不包含碰撞背景
/// </summary>
None,
/// <summary>
/// 碰撞透明背景
/// </summary>
Normal,
/// <summary>
/// 碰撞非透明背景
/// </summary>
WithBg,
}
#endregion
/// <summary>
/// 每个UI界面为一个Page,需设计层级关系但不区分管理,可自行从命名上区分,这里建议按照功能命名区分
/// </summary>
public abstract class UIPage
{
#region 变量
private class UIPageMono : MonoBehaviour { }
private UIPageMono m_UIPageMono;
//默认名为空字符串
public string m_Name = string.Empty;
//Page对应的UIRoot
public UIRoot m_UIRoot = default(UIRoot);
//page的id
public int m_id = -1;
//生成UI的类型,根据类型设定父物体
public UIHierarchy m_UIHierarchy = UIHierarchy.MainMenu;
//背景触发类型
public UICollider m_Collider = UICollider.None;
//加载UI的路径
public string m_UIPath = string.Empty;
//UI的物体
public GameObject m_GameObject;
public Transform m_Transform;
//所有Pages的Dictionary
private static Dictionary<string, UIPage> m_AllPages;
public static Dictionary<string, UIPage> AllPages
{ get { return m_AllPages; } }
//记录此ui加载模式,异步或者同步
private bool m_IsAsyncUI = false;
//Page是否打开
protected bool m_IsActived = false;
//刷新Page的数据
private object m_Data = null;
protected object Data { get { return m_Data; } }
//加载ui的委托
public static Func<string, Object> m_DelegateSyncLoadUI = null;
public static Action<string, Action<Object>> m_DelegateAsyncLoadUI = null;
#endregion
#region virtual api
//实例化Page
public virtual void Awake(GameObject go) { }
//刷新Page
public virtual void Refresh() { }
//打开该Page
public virtual void Active()
{
m_IsActived = true;
this.m_GameObject.SetActive(m_IsActived);
}
/// <summary>
/// 隐藏Page,不会清除数据
/// </summary>
public virtual void Hide()
{
this.m_GameObject.SetActive(false);
m_IsActived = false;
//隐藏时将此页的数据设为空
this.m_Data = null;
}
#endregion
#region internal api
private UIPage() { }
public UIPage(UIRoot uIRoot, UIHierarchy type, UICollider col)
{
if (uIRoot == null)
{
Debug.LogError("UIRoot is null");
return;
}
this.m_UIRoot = uIRoot;
this.m_UIHierarchy = type;
this.m_Collider = col;
this.m_Name = this.GetType().ToString();
if (m_UIPageMono == null)
m_UIPageMono = new UIPageMono();
//创建时执行Bind
UIBind.Bind();
}
/// <summary>
/// 同步ShowPage
/// </summary>
protected void Show()
{
if (this.m_GameObject == null && string.IsNullOrEmpty(m_UIPath) == false)
{
GameObject go = null;
if (m_DelegateSyncLoadUI != null)
{
Object o = m_DelegateSyncLoadUI(m_UIPath);
go = o != null ? GameObject.Instantiate(o) as GameObject : null;
}
else
{
go = GameObject.Instantiate(Resources.Load(m_UIPath)) as GameObject;
}
if (go == null)
{
Debug.LogError("[UI] Cant sync load your ui prefab.");
return;
}
go.name = go.name.Replace("(Clone)", "");
AnchorUIGameObject(go);
Awake(go);
m_IsAsyncUI = false;
}
AnchorUIGameObject(this.m_GameObject);
Active();
Refresh();
}
/// <summary>
/// 异步ShowPage
/// </summary>
protected void Show(Action callback)
{
m_UIPageMono.StartCoroutine(AsyncShow(callback));
}
IEnumerator AsyncShow(Action callback)
{
if (this.m_GameObject == null && string.IsNullOrEmpty(m_UIPath) == false)
{
GameObject go = null;
bool _loading = true;
m_DelegateAsyncLoadUI(m_UIPath, (o) =>
{
go = o != null ? GameObject.Instantiate(o) as GameObject : null;
AnchorUIGameObject(go);
Awake(go);
m_IsAsyncUI = true;
_loading = false;
Active();
Refresh();
if (callback != null) callback();
});
float _t0 = Time.realtimeSinceStartup;
while (_loading)
{
if (Time.realtimeSinceStartup - _t0 >= 10.0f)
{
Debug.LogError("[UI] WTF async load your ui prefab timeout!");
yield break;
}
yield return null;
}
}
else
{
AnchorUIGameObject(this.m_GameObject);
Active();
Refresh();
if (callback != null) callback();
}
}
/// <summary>
/// 设置Page的父级
/// </summary>
/// <param name="ui"></param>
protected void AnchorUIGameObject(GameObject ui)
{
if (m_UIRoot == null)
{
Debug.LogError("UIRoot is null");
return;
}
if (ui == null) return;
this.m_GameObject = ui;
this.m_Transform = ui.transform;
ui.transform.SetParent(null, false);
switch (m_UIHierarchy)
{
case UIHierarchy.BackGround:
ui.transform.SetParent(m_UIRoot.BackGround, false);
break;
case UIHierarchy.MainMenu:
ui.transform.SetParent(m_UIRoot.MainMenu, false);
break;
case UIHierarchy.SecondaryMenu:
ui.transform.SetParent(m_UIRoot.SecondaryMenu, false);
break;
case UIHierarchy.ThirdMenu:
ui.transform.SetParent(m_UIRoot.ThirdMenu, false);
break;
case UIHierarchy.Popup:
ui.transform.SetParent(m_UIRoot.Popup, false);
break;
case UIHierarchy.AlwaysFront:
ui.transform.SetParent(m_UIRoot.AlwaysFront, false);
break;
}
}
public override string ToString()
{
return ">Name:" + m_Name + ",ID:" + m_id + ",Type:" + m_UIHierarchy.ToString() + ",Collider:" + m_Collider.ToString();
}
public bool IsActive()
{
bool ret = m_GameObject != null && m_GameObject.activeSelf;
return ret || m_IsActived;
}
#endregion
#region static api
#region ShowPage的重载
private static void ShowPage<T>(Action callback, object pageData, bool isAsync) where T : UIPage, new()
{
Type t = typeof(T);
string pageName = t.ToString();
if (m_AllPages != null && m_AllPages.ContainsKey(pageName))
{
ShowPage(pageName, m_AllPages[pageName], callback, pageData, isAsync);
}
else
{
T instance = new T();
ShowPage(pageName, instance, callback, pageData, isAsync);
}
}
private static void ShowPage(string pageName, UIPage pageInstance, Action callback, object pageData, bool isAsync)
{
if (string.IsNullOrEmpty(pageName) || pageInstance == null)
{
Debug.LogError("[UI] show page error with :" + pageName + " maybe null instance.");
return;
}
if (m_AllPages == null)
{
m_AllPages = new Dictionary<string, UIPage>();
}
UIPage page = null;
if (m_AllPages.ContainsKey(pageName))
{
page = m_AllPages[pageName];
}
else
{
m_AllPages.Add(pageName, pageInstance);
page = pageInstance;
}
//if active before,wont active again.
//if (page.isActive() == false)
{
//before show should set this data if need. maybe.!!
page.m_Data = pageData;
if (isAsync)
page.Show(callback);
else
page.Show();
}
}
/// <summary>
/// Sync Show Page
/// </summary>
public static void ShowPage<T>() where T : UIPage, new()
{
ShowPage<T>(null, null, false);
}
/// <summary>
/// Sync Show Page With Page Data Input.
/// </summary>
public static void ShowPage<T>(object pageData) where T : UIPage, new()
{
ShowPage<T>(null, pageData, false);
}
public static void ShowPage(string pageName, UIPage pageInstance)
{
ShowPage(pageName, pageInstance, null, null, false);
}
public static void ShowPage(string pageName, UIPage pageInstance, object pageData)
{
ShowPage(pageName, pageInstance, null, pageData, false);
}
/// <summary>
/// Async Show Page with Async loader bind in 'TTUIBind.Bind()'
/// </summary>
public static void ShowPage<T>(Action callback) where T : UIPage, new()
{
ShowPage<T>(callback, null, true);
}
public static void ShowPage<T>(Action callback, object pageData) where T : UIPage, new()
{
ShowPage<T>(callback, pageData, true);
}
/// <summary>
/// Async Show Page with Async loader bind in 'TTUIBind.Bind()'
/// </summary>
public static void ShowPage(string pageName, UIPage pageInstance, Action callback)
{
ShowPage(pageName, pageInstance, callback, null, true);
}
public static void ShowPage(string pageName, UIPage pageInstance, Action callback, object pageData)
{
ShowPage(pageName, pageInstance, callback, pageData, true);
}
#endregion
#region ClosePage的重载
/// <summary>
/// 关闭Page
/// </summary>
public static void ClosePage(UIPage target)
{
if (target == null) return;
target.Hide();
}
/// <summary>
/// 关闭Page
/// </summary>
public static void ClosePage<T>() where T : UIPage
{
Type t = typeof(T);
string pageName = t.ToString();
if (m_AllPages != null && m_AllPages.ContainsKey(pageName))
{
ClosePage(m_AllPages[pageName]);
}
else
{
Debug.LogError(pageName + "havnt show yet!");
}
}
/// <summary>
/// 关闭Page
/// </summary>
public static void ClosePage(string pageName)
{
if (m_AllPages != null && m_AllPages.ContainsKey(pageName))
{
ClosePage(m_AllPages[pageName]);
}
else
{
Debug.LogError(pageName + " havnt show yet!");
}
}
#endregion
#endregion
}