Unity 轻量级UI层框架的设计

0 阅读1分钟

前言

       我们可能接触过很多成熟的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的子类中实现也可。具体的操作按项目的实际情况而定。