前言
我们可能接触过很多成熟的UI框架,例如MVC、MVP、MVVM等等,它们无一例外的都需要固定的代码格式,并尊重一定的规则才能应用起来。在当下市场的快速变化、开发效率要求较高的情况下,笨重的框架已经不适合项目的快速变化和目标要求。例如,在中轻度游戏、休闲类游戏或小游戏方面,要求的是快速研发、快速迭代、快速上线、快速试错,最后快速验证游戏是否有存在价值,从而快速调整各种战略,减少资源的浪费与提高生存发展的能力。
一个游戏可能有50%的功能都可能与UI层有关,所以设计一个通用的、高效的、学习成本低的UI应用框架是必选项,下面我们就来设计一款轻量级的UI框架。
一、UI节点的设计
游戏的UI展现,通常是首先展现主界面,然后在主界面上打开或弹出各种新的页面,所以我们首先得定义几个节点:根节点、UI摄像机节点、底层节点、中层节点、顶层节点、弹窗节点,每个节点我们把它定义对应一个unity layer:Bottom,Middle,Top,Popup。这样定义设计的目的是方便我们后面对打开的各种UI对象进行有序的管理,例如UI的遮挡、事件的穿透和屏蔽等。节点我们可以把它设计成一个Prefab,把它放到Resouces文件夹中。Prefab的设计如下图所示:
根节点上有一个脚本绑定,如图所示:
UI摄像机节点如图所示:
其它UI层级节点如图所示:
在UIRoot节点上的绑定脚本如下:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
namespace Simple.UI
{
public class UICanvasBinder : MonoBehaviour
{
public Camera UICamera;
public RectTransform BottomCanvas;
public RectTransform MiddleCanvas;
public RectTransform TopCanvas;
public RectTransform PopupCavas;
public RectTransform U3DCavas;
}
}
二、UI核心框代码设计(角色)
UI框的代码,我们要尽量设计得简单,但又不失去“框架”的特性。我们把UI框架的角色简化为两个:中介者角色和视图呈现角色。中介者角色用来管理UI对象的加载、管理、销毁,视图呈现角色用来展现UI的元素以及应用逻辑。这样中介者角色的功能就相对明确和简单,它与UI业务无关,它只负责UI对象的生命周期和获取应用管理,与UI业务相关的功能都放在视图展现角色上。我们可以叫这个中介者角色为"UIContext",视图呈现角色为"UIView",它们的代码设计如下:
UIContext.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Simple.Asset;
using System;
namespace Simple.UI
{
public abstract class UIContext : MonoBehaviour
{
public abstract UILayer Layer { get; }//UI层枚举
public abstract string AssetKey { get; }//UI资源的名称,prefab名称
public virtual bool Cache { get; } = false; //是否缓存
public virtual bool AssetCache { get; } = true;//addressable中是否缓存, 根据需要可以舍弃此字段
public UIView View { set; get; } //UI呈现角色
private AssetOwner _owner = new AssetOwner(); //此处是资源管理者,可换成适合你的组件。
private void Awake()
{
Init();
SetLayer();
}
public virtual GameObject LoadViewObject()//加载UI资源
{
var ah = _owner.Instantiate(AssetKey);// 加载UI资源prefab,可换成其它组件
var instance = ah.GetInstance();
return instance;
}
public virtual void UnloadViewObject()//卸载UI资源
{
if(!AssetCache)
_owner.ReleaseAll();
}
public virtual void OnClose() { }
public virtual void OnRemoveFromCache() { }
private void SetLayer()//设置UI对象所在的节点,所在的节点代表也所在的层级
{
UICanvasBinder mb = UIMgr.Instance.UIBinder;
if (Layer == UILayer.Bottom)
transform.SetParent(mb.BottomCanvas, false);
else if (Layer == UILayer.Middle)
transform.SetParent(mb.MiddleCanvas, false);
else if (Layer == UILayer.Top)
transform.SetParent(mb.TopCanvas,false);
else if (Layer == UILayer.PopupBox)
transform.SetParent(mb.PopupCavas, false);
else if (Layer == UILayer.U3D)
transform.SetParent(mb.U3DCavas);
}
private void Init()
{
var viewObject = LoadViewObject();
viewObject.name = UIUtility.GetUIViewTypeName(this);
var tv = UIUtility.GetUIViewType(this);
View =(UIView)viewObject.AddComponent(tv);
View.OnInitCompleted();
viewObject.transform.SetParent(transform, false);
viewObject.transform.localPosition = Vector3.zero;
}
public virtual void OnShow()//显示UI是回调,提供给给业务层接口
{
View.OnShow();
}
public void OnShow<T>(T data)
{
View.OnShow(data);
}
public void OnShow<T1, T2>(T1 data1, T2 data2)
{
View.OnShow(data1, data2);
}
public void OnShow<T1, T2, T3>(T1 data1, T2 data2, T3 data3)
{
View.OnShow(data1, data2, data3);
}
public void OnShow<T1, T2, T3, T4>(T1 data1, T2 data2, T3 data3, T4 data4)
{
View.OnShow(data1, data2, data3, data4);
}
public void OnHide()//隐藏UI时回调
{
View.OnHide();
}
}
}
UIView.cs:
using Gamelogic;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Simple.UI
{
public enum UILayer//定义的UI层级
{
Bottom,
Middle,
Top,
PopupBox,
U3D
}
public abstract class UIView : MonoBehaviour
{
public virtual void OnInitCompleted() { }//初始化结束时
public virtual void OnShow() { }//呈现时
public virtual void OnShow<T>(T data) { }
public virtual void OnShow<T1, T2>(T1 data1, T2 data2) { }
public virtual void OnShow<T1, T2, T3>(T1 data1, T2 data2, T3 data3) { }
public virtual void OnShow<T1, T2, T3, T4>(T1 data1, T2 data2, T3 data3, T4 data4) { }
public virtual void OnHide() { }//隐藏时
}
}
UIContext.cs中用到了反射获取UIView的功能,反射相关的代码在UIUtility中,如下:
UIUtility.cs:
using UnityEngine;
using System;
using System.Reflection;
namespace Simple.UI
{
public class UIUtility
{
private static string GetContextTypeFullName(UIContext context)
{
Type t = context.GetType();
string cname = t.FullName.Replace("Context","");
return cname;
}
public static UIData GetUIDataInstance(UIContext context)
{
string cname = GetUIDataTypeFullName(context);
Type t = context.GetType();
return (UIData)t.Assembly.CreateInstance(cname);
}
public static string GetUIDataTypeFullName(UIContext context)
{
return GetContextTypeFullName(context) + "Data";
}
public static string GetUIViewTypeFullName(UIContext context)
{
return GetContextTypeFullName(context) + "View";
}
public static string GetUIViewTypeName(UIContext context)
{
Type t = GetUIViewType(context);
return t.Name;
}
public static Type GetUIDataType(UIContext context)
{
string cname = GetUIDataTypeFullName(context);
Type t = context.GetType();
return t.Assembly.GetType(cname);
}
public static Type GetUIViewType(UIContext context)
{
string cname = GetUIViewTypeFullName(context);
Type t = context.GetType();
return t.Assembly.GetType(cname);
}
}
}
至此,我们完成了UI框架中最主要的两个角色代码设计,但还不能让这个框架应用起来。
三、UI核心框代码设计(管理者)
我们虽然完成了两个框架角色的设计,但要用上它们,如何用得更好,还得一个管理者角色,这个就是UIMgr,它的代码如下:
UIMgr.cs:
using UnityEngine;
using System.Collections.Generic;
namespace Simple.UI
{
public class UIMgr
{
private List<UIContext> _contexts = new List<UIContext>();//所有已打开的中介者
public Transform UIRoot { private set; get; }//定义的根节点
public UICanvasBinder UIBinder { private set; get; }
private static UIMgr _instance;
public static UIMgr Instance
{
get
{
if (_instance == null)
{
_instance = new UIMgr();
}
return _instance;
}
}
public void Init()//初始化UI,一般在游戏开始时调用
{
GameObject uiRoot = GameObject.Find("UIRoot");
if (uiRoot == null)
{
GameObject go = Resources.Load<GameObject>("UICoreRes/UIRoot");//将定义好的UI节点加载到场景中
uiRoot = GameObject.Instantiate<GameObject>(go);
uiRoot.name = "UIRoot";
}
UIRoot = uiRoot.transform;
UIBinder = uiRoot.GetComponent<UICanvasBinder>();
GameObject.DontDestroyOnLoad(uiRoot);//注意,是夸场景使用,游戏不退出不销毁
}
public bool Has<T>() where T : UIContext//
{
for (int i = 0; i < _contexts.Count; i++)
{
if (_contexts[i].GetType() == typeof(T))
return true;
}
return false;
}
public T Get<T>() where T : UIContext
{
for (int i = 0; i < _contexts.Count; i++)
{
if (_contexts[i].GetType() == typeof(T))
return _contexts[i] as T;
}
return null;
}
private T Add<T>() where T : UIContext
{
T t;
string name = typeof(T).Name;
GameObject go = new GameObject(name);
go.layer = LayerMask.NameToLayer("UI");//设置为统一标识层
RectTransform rectTran = go.AddComponent<RectTransform>();
rectTran.sizeDelta = new Vector2(0, 0);
rectTran.anchorMin = new Vector2(0, 0);
rectTran.anchorMax = new Vector2(1, 1);
t = go.AddComponent<T>();//绑定上中价者角色
_contexts.Add(t);
return t;
}
public T Open<T>() where T : UIContext//打开中介者角色
{
T t = Get<T>();
if (t == null)
t = Add<T>();
else
{
if (!t.gameObject.activeSelf)
t.gameObject.SetActive(true);
}
t.OnShow();
return t;
}
public T Open<T, D>(D data) where T : UIContext
{
T t = Get<T>();
if (t == null)
t = Add<T>();
else
{
if (!t.gameObject.activeSelf)
t.gameObject.SetActive(true);
}
t.OnShow(data);
return t;
}
public T Open<T, D1,D2>(D1 data1, D2 data2) where T : UIContext
{
T t = Get<T>();
if (t == null)
t = Add<T>();
else
{
if (!t.gameObject.activeSelf)
t.gameObject.SetActive(true);
}
t.OnShow(data1, data2);
return t;
}
public T Open<T, D1, D2, D3>(D1 data1, D2 data2, D3 data3) where T : UIContext
{
T t = Get<T>();
if (t == null)
t = Add<T>();
else
{
if (!t.gameObject.activeSelf)
t.gameObject.SetActive(true);
}
t.OnShow(data1, data2, data3);
return t;
}
public T Open<T, D1, D2, D3, D4>(D1 data1, D2 data2, D3 data3, D4 data4) where T : UIContext
{
T t = Get<T>();
if (t == null)
t = Add<T>();
else
{
if (!t.gameObject.activeSelf)
t.gameObject.SetActive(true);
}
t.OnShow(data1, data2, data3, data4);
return t;
}
public void Close<T>() where T : UIContext//关闭中介者角色
{
T context = Get<T>();
if (context == null) return;
context.OnClose();
if (!context.Cache)//没缓存的情况下直接卸载资源
{
context.OnHide();
_contexts.Remove(context);
context.OnRemoveFromCache();
context.UnloadViewObject();
GameObject.Destroy(context.gameObject);
}
else
{
context.OnHide();
context.gameObject.SetActive(false);
}
}
public void DestroyAll()
{
foreach(var context in _contexts)
{
context.OnHide();
context.OnRemoveFromCache();
context.UnloadViewObject();
GameObject.Destroy(context.gameObject);
}
_contexts.Clear();
}
public List<UIContext> GetOpenedContexts()
{
var contexts = new List<UIContext>();
foreach(var context in _contexts)
{
if(context.gameObject.activeSelf)
contexts.Add(context);
}
return contexts;
}
}
}
至此我们基本完成了一个基于Unity的轻量级UI层框架的核心研发。
四、如何使用
我们以一个Loading界面为例来说明如何使用这个框架。首先我们要建立两个角色类:LoadingContext.cs、LoadingView.cs,分别继续自UIContext和UIView,具体代码如下所示:
LoadingContext.cs:
using UnityEngine;
using Simple.UI;
namespace Gamelogic
{
public class LoadingContext : UIContext
{
public override UILayer Layer => UILayer.Bottom;
public override string AssetKey => "Pre_UI_Loading";
public override bool Cache => false;
public new LoadingView View { get => (LoadingView)base.View; set => base.View = value; }
public override GameObject LoadViewObject()//重写基类的加载逻辑,灵活控制
{
//return base.LoadViewObject();
var ui = Resources.Load<GameObject>($"Prefabs/Loading/{AssetKey}");//从文件夹中加载UI对象
var go = GameObject.Instantiate( ui );
return go;
}
public override void UnloadViewObject()
{
base.UnloadViewObject();
}
public override void OnClose()
{
base.OnClose();
}
public override void OnRemoveFromCache()
{
base.OnRemoveFromCache();
}
}
}
LoadingView.cs:
using UnityEngine;
using UnityEngine.UI;
using Simple.UI;
using System.Collections.Generic;
using System;
namespace Gamelogic
{
public class LoadingView : UIView
{
private UIGetter _getter;
private Slider _sldLoading;
private Text _txtVersion;
private Text _txtNum;
private void Awake()
{
//获取prefab的UI控件
_getter = GetComponent<UIGetter>();
_sldLoading = _getter.GetElement("sldLoading").GetComponent<Slider>();
_txtVersion = _getter.GetElement("txtVersion").GetComponent<Text>();
_txtNum = _getter.GetElement("txtNum").GetComponent<Text>();
_txtVersion.text = $"v{Application.version}";
}
//数据的来源,可以交给第三方或在类中实现
public void UpdateLoading(float percent)
{
if (_sldLoading != null)
_sldLoading.value = percent;
if (_txtNum != null)
{
var p = (int)(percent * 100);
if(p > 100)
p = 100;
_txtNum.text = $"{p}%";
}
}
}
}
接着,我们在打开UI时,必须先初始化UI框架,最好是在游戏戏启动的时候初始化,代码如下:
using UnityEngine;
using System.Collections;
using Simple.UI;
namespace Gamelogic
{
public class GameSetting : MonoBehaviour
{
private void Awake()
{
DontDestroyOnLoad(this.gameObject);
DontDestroyOnLoad(GameObject.Find("EventSystem"));
UIMgr.Instance.Init(); //初始化UI框架
}
// Use this for initialization
void Start()
{
var loadingUI = UIMgr.Instance.Open<LoadingContext>();//打开Loading界面
loadingUI.View.UpdateLoading(0.1f);//更新界面
//UIMgr.Instance.Close<LoadingContext>();//关闭loading界面
}
// Update is called once per frame
void Update()
{
}
}
}
五、总结
总的来说,这个UI层框架只有三个角色:管理者、中介者、呈现者。这三个角色变动最频繁的应该是呈现者角色,主要体现在UI的设计、布局和更新换代方面。换句话说,开发人员大部分的时间都是专注于UIView的子类开发上。开发人员可以直接继续沿用Unity的相关功能,并不需要写中间件,例如启用协程。至于数据的存储、呈现操作部分,完全是交给另外的模块按需实现,然后提供相关的API供UIView调用即可,或者是直接在UIView的子类中实现也可。具体的操作按项目的实际情况而定。