RhinoFreak 发表于 2022-9-21 14:26

Unity游戏开发笔记-UI框架

谈到UI,所有游戏前端程序员都深恶痛绝。因为这是每个开发的必经之路。拼UI,接协议,写逻辑,做动画,各种模块之前的相互调用。嘤嘤嘤~想想都觉得麻烦,但是这又是不可避免的,毕竟UI也是游戏很重要的一部分啊。一个合理的UI框架的设计,不仅能提高程序的开发效率,在应对后续策划五花八门的需求的时候,也能表现的从容不迫。UI框架的设计真的很重要~大家不要小瞧了这件事情~
       下面步入正题,我们通过简单的图来梳理下UI框架是什么,需要实现那些东西:


上面是一个简单的Ui基类设计!提供了一些常用的属性以及方法!我们每一个界面都应该继承这个类实现基础接口!这里我们对UI的生命周期做了一个简单的划分,方便我们在实现功能的时候有一个明确的思路,粗暴一点直接上代码:
// **************************************
//
// 文件名(MUIBase.cs):
// 功能描述("UI基类"):
// 作者(Max1993):
// 日期(2019/5/1921:26):
//
// **************************************
//
using UnityEngine;
using System.Collections;

namespace MFrameWork
{
    //GameObject Loaded CallBack 物体加载回掉
    public delegate void GameObjectLoadedCallBack(GameObject obj);

    /// <summary>
    /// UI层级
    /// </summary>
    public enum MUILayerType
    {
      Top,
      Upper,
      Normal,
      Hud
    }

    /// <summary>
    /// 加载UI的方式
    /// </summary>
    public enum MUILoadType
    {
      SyncLoad,
      AsyncLoad,
    }

    public abstract class MUIBase
    {
      /// <summary>
      /// 是否加载完成的标志位
      /// </summary>
      protected bool m_isInited;

      /// <summary>
      /// UI名字
      /// </summary>
      protected string m_uiName;

      /// <summary>
      /// 在关闭的时候是否缓存UI 默认不缓存
      /// </summary>
      protected bool m_isCatchUI=false;

      /// <summary>
      /// UI的实例化GamObejct
      /// </summary>
      protected GameObject m_uiGameObject;

      /// <summary>
      /// 设置UI可见性状态
      /// </summary>
      protected bool m_active = false;

      /// <summary>
      /// 加载完成的回调
      /// </summary>
      protected GameObjectLoadedCallBack m_callBack;

      /// <summary>
      /// UI的资源全路径
      /// </summary>
      protected string m_uiFullPath = "";

      //UILayerType UI层类型
      protected MUILayerType m_uiLayerType;

      //UI的加载方式
      protected MUILoadType m_uiLoadType;

      public string UIName
      {
            get { return m_uiName;}
            set
            {
                m_uiName = value.EndsWith(MPathUtils.UI_PREFAB_SUFFIX) ? m_uiName = value : m_uiName = value + MPathUtils.UI_PREFAB_SUFFIX;
                m_uiFullPath = MPathUtils.UI_MAINPATH + "/" + m_uiName;
            }
      }

      public bool IsCatchUI
      {
            get { return m_isCatchUI; }
            set
            {
                m_isCatchUI = value;
            }
      }

      public GameObject UIGameObject
      {
            get { return m_uiGameObject; }
            set { m_uiGameObject = value; }
      }

      public bool Active
      {
            get { return m_active; }
            set
            {
                m_active = value;
                if(m_uiGameObject!=null)
                {
                  m_uiGameObject.SetActive(value);
                  if(m_uiGameObject.activeSelf)
                  {
                        OnActive();   
                  }
                  else
                  {
                        OnDeActive();
                  }
                }
            }
      }

      public bool IsInited { get { return m_isInited; } }

      protected MUIBase(string uiName,MUILayerType layerType,MUILoadType loadType = MUILoadType.SyncLoad)
      {
            UIName = uiName;
            m_uiLayerType = layerType;
            m_uiLoadType = loadType;
      }

      public virtual void Init()
      {
            if(m_uiLoadType == MUILoadType.SyncLoad)
            {
                GameObject go = MObjectManager.singleton.InstantiateGameObeject(m_uiFullPath);
                OnGameObjectLoaded(go);
            }
            else
            {
                MObjectManager.singleton.InstantiateGameObejectAsync(m_uiFullPath,delegate (string resPath, MResourceObjectItem mResourceObjectItem,object[] parms)
                {
                  GameObject go = mResourceObjectItem.m_cloneObeject;
                  OnGameObjectLoaded(go);
                },LoadResPriority.RES_LOAD_LEVEL_HEIGHT);
            }
      }

      private void OnGameObjectLoaded(GameObject uiObj)
      {
            if(uiObj == null)
            {
                MDebug.singleton.AddErrorLog("UI加载失败了老铁~看看路径 ResPath: " + m_uiFullPath);
                return;
            }
            m_uiGameObject = uiObj;
            m_isInited = true;
            SetPanetByLayerType(m_uiLayerType);
            m_uiGameObject.transform.localPosition = Vector3.zero;
            m_uiGameObject.transform.localScale = Vector3.one;
      }

      public virtual void Uninit()
      {
            m_isInited = false;
            m_active = false;
            if (m_isCatchUI)
            {
                //资源并加入到资源池
                MObjectManager.singleton.ReleaseObject(m_uiGameObject);
            }
            else
            {
                //彻底清除Object资源
                MObjectManager.singleton.ReleaseObject(m_uiGameObject, 0, true);
            }
      }

      protected abstract void OnActive();

      protected abstract void OnDeActive();

      public virtual void Update(float deltaTime)
      {
            
      }

      public virtual void LateUpdate(float deltaTime)
      {
            
      }

      public virtual void OnLogOut()
      {
            
      }

      protected void SetPanetByLayerType(MUILayerType layerType)
      {
            switch(m_uiLayerType)
            {
                case MUILayerType.Top:
                  m_uiGameObject.transform.SetParent(MUIManager.singleton.MTransTop);
                  break;
                case MUILayerType.Upper:
                  m_uiGameObject.transform.SetParent(MUIManager.singleton.MTransUpper);
                  break;
                case MUILayerType.Normal:
                  m_uiGameObject.transform.SetParent(MUIManager.singleton.MTransNormal);
                  break;
                case MUILayerType.Hud:
                  m_uiGameObject.transform.SetParent(MUIManager.singleton.MTransHUD);
                  break;
            }
      }
    }
}
UI的管理类,说白了就是对UIBase的一个管理类。提供一些常用的接口函数,管理UI层级,创建,销毁,缓存等,加单来说如下图,上图:


// **************************************
//
// 文件名(MUIManager.cs):
// 功能描述("UI核心管理类"):
// 作者(Max1993):
// 日期(2019/5/1921:26):
//
// **************************************
//
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System.Collections.Generic;

namespace MFrameWork
{
    public class MUIManager : MSingleton<MUIManager>
    {
      /// <summary>
      /// 核心的管理所有UI的Dictionary
      /// </summary>
      private Dictionary<string, MUIBase> m_uiDict;

      /// <summary>
      /// 摄像机UICamera
      /// </summary>
      private Camera m_uiCamera;

      private GameObject m_uiRoot;
      private Transform m_transNormal;
      private RectTransform m_rectTransNormal;
      private Transform m_transTop;
      private RectTransform m_rectTransTop;
      private Transform m_transUpper;
      private RectTransform m_rectTransUpper;
      private Transform m_transHUD;
      private RectTransform m_rectTransHUD;

      //以下是一些基础的层级位置信息
      public GameObject MUIRoot { get { return m_uiRoot; } }
      public Transform MTransNormal { get { return m_transNormal; } }
      public RectTransform MRectTransNormal { get { return m_rectTransNormal; } }
      public Transform MTransUpper { get { return m_transUpper; } }
      public RectTransform MRectTransUpper { get { return m_rectTransUpper; } }
      public Transform MTransTop { get { return m_transTop; } }
      public RectTransform MRectTransTop { get { return m_rectTransTop; } }
      public Transform MTransHUD { get { return m_transHUD; } }
      public RectTransform MRectTransHUD { get { return m_rectTransHUD; } }

      public Camera MUICamera
      {
            get
            {
                return m_uiCamera;
            }
      }

      public override bool Init()
                {
            return InitUIInfo() && UIRegister();
      }

      /// <summary>
      /// 初始化一些常用的UI信息
      /// </summary>
      /// <returns></returns>
      public bool InitUIInfo()
      {
            m_uiDict = new Dictionary<string, MUIBase>();
            m_uiRoot = MObjectManager.singleton.InstantiateGameObeject(MPathUtils.UI_ROOTPATH);
            if (m_uiRoot == null)
            {
                MDebug.singleton.AddErrorLog("初始化UIManager 失败了~");
                return false;
            }
            m_uiRoot.name = "UIRoot";
            m_uiRoot.SetActive(true);
            m_transNormal = m_uiRoot.transform.Find("NormalLayer");
            m_rectTransNormal = m_transNormal.gameObject.GetComponent<RectTransform>();
            m_transTop = m_uiRoot.transform.Find("TopLayer");
            m_rectTransTop = m_transTop.gameObject.GetComponent<RectTransform>();
            m_transUpper = m_uiRoot.transform.Find("UpperLayer");
            m_rectTransUpper = m_transUpper.gameObject.GetComponent<RectTransform>();
            m_transHUD = m_uiRoot.transform.Find("HudLayer");
            m_rectTransHUD = m_transHUD.gameObject.GetComponent<RectTransform>();
            m_uiCamera = m_uiRoot.transform.Find("Camera").GetComponent<Camera>();
            GameObject.DontDestroyOnLoad(m_uiRoot);
            return true;
      }

      public const string LOGON_CONTROLLER = "LoginPanel.prefab";
      /// <summary>
      /// 在C#层实现逻辑的UI进行注册注册
      /// </summary>
      /// <returns></returns>
      private bool UIRegister()
      {
            m_uiDict.Add(LOGON_CONTROLLER, new LogonController());
            return true;
      }

      public override void UnInit()
                {
            if (m_uiRoot)
            {
                MObjectManager.singleton.ReleaseObjectComopletly(m_uiRoot);
                m_uiRoot = null;
                m_transNormal = null;
                m_rectTransNormal = null;
                m_transTop = null;
                m_rectTransTop = null;
                m_transUpper = null;
                m_rectTransUpper = null;
                m_transHUD = null;
                m_rectTransHUD = null;
                m_uiCamera = null;
            }
                }

                public override void OnLogOut()
                {
            base.OnLogOut();
                }

      /// <summary>
      /// 打开一个UI的接口
      /// </summary>
      /// <param name="uiName"></param>
      /// <returns></returns>
      public MUIBase ActiveUI(string uiName)
      {
            MUIBase mUIBase = GetUI(uiName);
            if (mUIBase == null)
            {
                Debug.LogError("UIDic里面没有这个UI信息 UIName:"+ uiName);
                return null;
            }

            if (!mUIBase.IsInited)
            {
                mUIBase.Init();
            }

            return mUIBase;
      }

      /// <summary>
      /// 关闭一个UI的接口
      /// </summary>
      /// <param name="uiName"></param>
      public void DeActiveUI(string uiName)
      {
            MUIBase mUIBase = GetUI(uiName);
            if (mUIBase == null)
            {
                Debug.LogError("UIDic里面没有这个UI信息 UIName:" + uiName);
                return;
            }

            if(mUIBase.IsInited)
            {
                if (mUIBase.Active)
                {
                  mUIBase.Active = false;
                }
                mUIBase.Uninit();
            }

      }

      /// <summary>
      /// 获取一个UI的接口
      /// </summary>
      /// <param name="uiName"></param>
      /// <returns></returns>
      public MUIBase GetUI(string uiName)
      {
            MUIBase mUIBase = null;
            m_uiDict.TryGetValue(uiName, out mUIBase);
            return mUIBase;
      }

      public T GetUI<T>(string uiName) where T : MUIBase
      {
            MUIBase mUIBase = null;
            if (m_uiDict.TryGetValue(uiName, out mUIBase))
            {
                if (mUIBase is T)
                {
                  return (T)mUIBase;
                }
            }
            return null;
      }

      /// <summary>
      /// 关闭所有UI的接口
      /// </summary>
      public void DeActiveAll()
      {
            foreach (KeyValuePair<string, MUIBase> pair in m_uiDict)
            {
                DeActiveUI(pair.Key);
            }
      }
      
      /// <summary>
      /// Update方法
      /// </summary>
      /// <param name="delta"></param>
      public void Update(float delta)
      {
            foreach (var mUIBase in m_uiDict.Values)
            {
                mUIBase.Update(delta);
            }
      }

      /// <summary>
      /// LateUpdate方法
      /// </summary>
      /// <param name="delta"></param>
      public void LateUpdate(float delta)
      {
            foreach (var mUIBase in m_uiDict.Values)
            {
                mUIBase.LateUpdate(delta);
            }
      }

      /// <summary>
      /// 注销方法
      /// </summary>
      public void OnLogout()
      {
            foreach (var mUIBase in m_uiDict.Values)
            {
                mUIBase.OnLogOut();
            }
            if (m_uiCamera)
            {
                m_uiCamera.enabled = false;
            }
      }
    }
}
然后就是每一个独立的UI,创建类继承自MuiBase,实现一些核心接口,完成业务相关功能就可以了。如下所示:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

namespace MFrameWork
{
    public class LogonController : MUIBase
    {
      public GameObject BtnStart;
      public GameObject BtnExit;

      public LogonController() : base("LoginPanel", MUILayerType.Normal)
      {

      }

                public override void Init()
                {
            base.Init();
            BtnStart = m_uiGameObject.transform.Find("BtnLogin").gameObject;
            BtnExit= m_uiGameObject.transform.Find("BtnExit").gameObject;

            BtnStart.GetComponent<Button>().onClick.AddListener(delegate
            {
                Debug.Log("Start");
            });

            BtnExit.GetComponent<Button>().onClick.AddListener(delegate
            {
                Debug.Log("Exit");
            });
                }

                protected override void OnActive()
      {
            Debug.Log("Active");
      }

      protected override void OnDeActive()
      {
            Debug.Log("OnDeActive");
      }

    }
}嗯,UI框架基本上介绍的差不多了。其实有很多接口需求,都是需要在实际项目开发中结合实际遇到的问题,慢慢加进去的,但是基础的东西就这些了。这里其实还有几点没有介绍到,就是对数据层的管理还有复用组件的封装。
复用组件其实不是一个独立的UI,他就是一个节点下的Object,但是这个Object可能在游戏中的各个地方都可能会用到,那么我们就很有必要,对这个做下逻辑的封装管理,提高业务开发的速度,以及避免重复代码的编写。
数据和显示层的剥离也是一个比较必要的框架设计部分,各司其职,这样代码结构看起来就会清晰很多。
其实当前商业化游戏,业务逻辑基本都是在Lua层实现的。下一篇文章,我会写一下在Lua层的封装,以及上面提到的两个点的做一个具体的解释。

APSchmidt 发表于 2022-9-21 14:27

多谢

NoiseFloor 发表于 2022-9-21 14:34

嗯,简单的思路介绍~希望能帮到你~

super1 发表于 2022-9-21 14:37

这个类图是用什么软件画的?

maltadirk 发表于 2022-9-21 14:43

你那个在UI管理类中去Update 是有点多余了,首先UI加载出来,必须是继承了MonoMehaviou类的,也是必须有GameObject 的, 完全可以根据自己的UI弹窗自己去管理的,比如你的UIBase类就是一个基类也是必须要继承MonoMehaviou否则你UI怎么显示,怎么调取SetAction函数。 所以UI管理类中的Update完全是多余,而且你还用foreach遍历,不找死么,无形增加开销。

super1 发表于 2022-9-21 14:51

为什么一定要继承mono?照你你这样说那些用lua的,ui都做不了了?

HuldaGnodim 发表于 2022-9-21 14:56

作者这明显是一套不继承mono的ui解决方案,看清楚再说
页: [1]
查看完整版本: Unity游戏开发笔记-UI框架