在 ILRuntime 的基础上,搭建一个简单的UI系统(一)UIPanel

前言

在前面三篇文章中,我们简单的搭建好了ILRuntime的使用环境,然而还并没有实现具体的功能。所以在这篇文章中我们首先简单的实现下UI模块相关的功能。(暂时不考虑AB包系统,对象池系统等等,同时本篇先是简单的实现UI显示以及跳转,更多的功能在后续进行补充)

GitHub地址:https://github.com/luckyWjr/ILRuntimeDemo 方便代码查看(由于代码量比较多,下面只会贴出部分代码,感兴趣的同学还是直接看工程吧~有什么更好的意见欢迎提出指正)

 

思路

在实现代码前,我们首先要清楚我们的需求,UI系统的功能大致有加载UI,显示UI,跳转UI,UI层级管理等等。

通常我们会把一个完整的界面当成一个整体(一个prefab),比如登陆页面,主页面,商城页面等等,它们可以都继承于一个基类(暂定为UIPanel),主要功能就是显示隐藏界面,获取界面内的组件等等。

在一些界面中往往有些组件需要我们单独管理的,比如用户信息栏,商城物品的Item等,它们也可以都继承于一个基类(暂定为UIView,同时UIPanel继承于UIView),主要功能就是加载prefab,数据的显示等。

然后我们建立两个管理器,UIPanelManager和UIViewManager分别进行管理。UIPanelManager主要管理界面的显示隐藏和跳转,UIViewManager主要维护UIView的生命周期。

由于我们生成一个UIPanel的子类就需要加载对应的Prefab,比如要显示登陆界面,我们要实例化LoginPanel,同时加载LoginPanel.prefab,那么我们如何在实例化UIPanel的时候就得知其对应的Prefab是哪个呢,我们可以使用Attribute功能来实现,在每个UIPanel中进行配置对应的prefab的名称,具体代码看下文。

简单效果如图

 

代码实现

由于UI系统的逻辑后期变动会比较大,也容易出BUG,更新也会很频繁,所以我们将这部分的相关代码全部放在Hotfix部分,以便后续维护更新。目录结构大致如下

首先创建一个IView接口,用于生命周期

namespace Hotfix.UI
{
    public interface IView

    {
        //初始化,只在prefab被创建的时候执行一次
        void Init();
        //每次界面显示的时候执行
        void Show();
        void Update();
        void LateUpdate();
        void FixedUpdate();
        //界面被隐藏的时候执行,再次显示会调用Show方法
        void Hide();
        //销毁的时候执行
        void Destroy();
    }
}

然后创建UIView类实现IView接口,主要功能是加载和销毁gameobject,并在Show和Hide方法中进行显示和隐藏。

namespace Hotfix.UI
{
    public class UIView : IView
    {
        //需要加载的prefab的路径,也作为唯一标识符
        public string url { private set; get; }

        public GameObject gameObject { private set; get; }
        public Transform transform { private set; get; }
        public RectTransform rectTransform { private set; get; }

        //是否加载完成
        public bool isLoaded { get { return gameObject != null; } }

        //是否显示
        public bool isVisible
        {
            get
            {
                return isLoaded && gameObject.activeSelf;
            }
            set
            {
                if (isLoaded)
                    gameObject.SetActive(value);
            }
        }

        //若为true,将在下一帧销毁gameobject
        internal bool isWillDestroy;

        public UIView(string url)
        {
            this.url = url;
        }

        public virtual void Init()
        {
            isVisible = false;
        }

        public virtual void Show()
        {
            isVisible = true;
        }

        ......

        public virtual void Hide()
        {
            isVisible = false;
        }

        public virtual void Destroy()
        {
            isWillDestroy = true;
            if (isVisible)
            {
                Hide();
            }
        }

        //销毁gameobject
        public void DestroyImmediately()
        {
            if (!isWillDestroy)
            {
                Destroy();
            }
            GameObject.Destroy(gameObject);
            gameObject = null;
            transform = null;
            rectTransform = null;
        }

        //加载prefab
        public virtual void Load(Action callback = null)
        {
            gameObject = GameObject.Instantiate(Resources.Load(url)) as GameObject;
            if (gameObject != null)
            {
                transform = gameObject.transform;
                rectTransform = gameObject.GetComponent<RectTransform>();

                Init();
                callback?.Invoke();
            }
        }
    }
}

然后编写UIPanel,继承于UIView

namespace Hotfix.UI
{
    public class UIPanel : UIView
    {
        //UIPanel间的自定义传递数据
        public object data;

        //前一个UIPanel,用于隐藏自己的时候,Show前者
        public UIPanel previousPanel;

        public UIPanel(string url) : base(url)
        {
        }

        public override void Destroy()
        {
            base.Destroy();
            previousPanel = null;
        }
    }
}

接着我们编写UIViewManager,用于管理UIView,主要用于生成和获取UIView,管理所有UIView的生命周期

using Hotfix.UI;
using System;
using System.Collections.Generic;

namespace Hotfix.Manager
{
    public class UIViewManager : ManagerBase<UIViewManager>
    {
        //存放所有在场景中的UIView
        List<UIView> m_UIViewList;

        public override void Init()
        {
            base.Init();
            m_UIViewList = new List<UIView>();
        }

        public override void Update()
        {
            base.Update();

            for (int i = 0; i < m_UIViewList.Count; i++)
            {
                //销毁UIView
                if (m_UIViewList[i].isWillDestroy)
                {
                    m_UIViewList[i].DestroyImmediately();
                    m_UIViewList.RemoveAt(i);
                    i--;
                    continue;
                }

                if (m_UIViewList[i].isVisible)
                {
                    m_UIViewList[i].Update();
                }
            }
        }

        ......


        //创建UIView
        public UIView CreateView(Type type, params object[] args)
        {
            UIView view = Activator.CreateInstance(type, args) as UIView;
            m_UIViewList.Add(view);
            return view;
        }

        public void DestroyAll()
        {
            for (int i = 0; i < m_UIViewList.Count; i++)
                m_UIViewList[i].Destroy();
        }
    }
}

接下来是比较重要的一点了,在前面的UIView当中,我们通过url这个路径来加载prefab,那么我们实例化一个UIPanel的时候,比如LoginPanel,MainPanel,我们如何仅仅通过Type知道每个UIPanel对应的prefab,即url的值。

我们的解决思路是使用自定义的Attribute,叫ManagerAtrribute,里面会有个string的值用于存储最基础的自定义数据。同时对于用于这类Attribute的类,他们的管理器也要进行特殊处理。

using System;
namespace Hotfix.Manager
{
    public interface IAttribute
    {
        //检测符合IAttribute的类
        void CheckType(Type type);
        //获取Attribute信息
        AttributeData GetAtrributeData(string attrValue);
        //生成被管理类的实例,管理类为T,被管理的类为T2
        T2 CreateInstance<T2>(string attrValue) where T2 : class;
        //获取被管理类的构造函数参数
        object[] GetInstanceParams(AttributeData data);
    }
}
namespace Hotfix.Manager
{
    public class AttributeData
    {
        public ManagerAttribute attribute;
        public Type type;
    }

    public class ManagerAttribute : Attribute
    {
        public string value { get; protected set; }
        public ManagerAttribute(string value)
        {
            this.value = value;
        }
    }

    public class ManagerBaseWithAttr<T, V> : ManagerBase<T>, IAttribute where T : IManager, new() where V : ManagerAttribute
    {
        protected Dictionary<string, AttributeData> m_atrributeDataDic;

        protected ManagerBaseWithAttr()
        {
            m_atrributeDataDic = new Dictionary<string, AttributeData>();
        }

        public virtual void CheckType(Type type)
        {
            var attrs = type.GetCustomAttributes(typeof(V), false);
            if (attrs.Length > 0)
            {
                var attr = attrs[0];
                if (attr is V)
                {
                    var _attr = (V)attr;
                    SaveAttribute(_attr.value, new AttributeData() { attribute = _attr, type = type });
                }
            }
        }

        public AttributeData GetAtrributeData(string attrValue)
        {
            AttributeData classData = null;
            m_atrributeDataDic.TryGetValue(attrValue, out classData);
            return classData;
        }

        public void SaveAttribute(string name, AttributeData data)
        {
            m_atrributeDataDic[name] = data;
        }

        public T2 CreateInstance<T2>(string attrValue) where T2 : class
        {
            var data = GetAtrributeData(attrValue);
            if (data == null)
            {
                Debug.LogError("没有找到:" + attrValue + " -" + typeof(T2).Name);
                return null;
            }
            if (data.type != null)
            {
                object[] p = GetInstanceParams(data);
                if (p.Length == 0)
                    return Activator.CreateInstance(data.type) as T2;
                else
                    return Activator.CreateInstance(data.type, p) as T2;
            }
            return null;
        }

        public virtual object[] GetInstanceParams(AttributeData data)
        {
            return new object[] { data.attribute.value };
        }
    }
}

然后我们的UIPanelManager会继承于上面的ManagerBaseWithAttr,同时创建一个UIAttribute继承于ManagerAttribute,我们的UIPanel的构造函数参数会对应UIAttribute的参数(如有需要可以在子类Attribute中添加其他自己需要的参数,然后在子类ManagerBaseWithAttr中改写GetInstanceParams方法)

下面是我们的UIPanelManager的代码,目前的主要功能就是生成,显示,隐藏和销毁UIPanel。

namespace Hotfix.Manager
{
    public class UIPanelManager : ManagerBaseWithAttr<UIPanelManager, UIAttribute>
    {
        public UIPanel currentPanel;//当前显示的页面

        Dictionary<string, UIPanel> m_UIPanelDic;//存放所有存在在场景中的UIPanel
        Transform m_UICanvas;

        public override void Init()
        {
            base.Init();
            m_UIPanelDic = new Dictionary<string, UIPanel>();
            m_UICanvas = GameObject.Find("Canvas").transform;
        }

        public void ShowPanel<T>() where T : UIPanel
        {
            ShowPanel<T>(null, null);
        }

        public void ShowPanel<T>(Action<T> callback) where T : UIPanel
        {
            ShowPanel(callback, null);
        }

        public void ShowPanel<T>(object data) where T : UIPanel
        {
            ShowPanel<T>(null, data);
        }

        //显示一个UIPanel,参数为回调和自定义传递数据
        public void ShowPanel<T>(Action<T> callback, object data) where T : UIPanel
        {
            string url = GetUrl(typeof(T));
            if (!string.IsNullOrEmpty(url))
            {
                LoadPanel(url, data, () =>
                {
                    var panel = ShowPanel(url);
                    callback?.Invoke(panel as T);
                });
            }
        }

        //显示UIPanel
        UIPanel ShowPanel(string url)
        {
            if (m_UIPanelDic.TryGetValue(url, out UIPanel panel))
            {
                panel = m_UIPanelDic[url];
                if (!panel.isVisible)
                {
                    currentPanel?.Hide();
                    panel.previousPanel = currentPanel;
                    panel.Show();
                    currentPanel = panel;
                }
                else
                    Debug.Log("UIPanel is visible:" + url);
            }
            else
                Debug.LogError("UIPanel not loaded:" + url);
            return panel;
        }

        //加载UIPanel对象
        public void LoadPanel(string url, object data, Action callback)
        {
            if (m_UIPanelDic.TryGetValue(url, out UIPanel panel))
            {
                if (panel.isLoaded)
                    callback?.Invoke();
            }
            else
            {
                panel = CreatePanel(url);
                if (panel == null)
                    Debug.LogError("UIPanel not exist: " + url);
                else
                {
                    panel.data = data;
                    m_UIPanelDic[url] = panel;
                    panel.Load(() =>
                    {
                        if (panel.isLoaded)
                        {
                            panel.rectTransform.SetParentAndResetTrans(m_UICanvas);
                            callback?.Invoke();
                        }
                        else
                            m_UIPanelDic.Remove(url);
                    });
                }
            }
        }

        //实例化UIPanel对象
        UIPanel CreatePanel(string url)
        {
            var data = GetAtrributeData(url);
            if (data == null)
            {
                Debug.LogError("Unregistered UIPanel, unable to load: " + url);
                return null;
            }
            var attr = data.attribute as UIAttribute;
            var panel = UIViewManager.Instance.CreateView(data.type, attr.value) as UIPanel;

            或者
            //var panel = CreateInstance<UIPanel>(url);
            //UIViewManager.Instance.AddUIView(panel as UIView);
            return panel;
        }

        //隐藏当前显示的UIPanel
        public void HidePanel()
        {
            currentPanel.Hide();
            //显示上一层页面
            if (currentPanel.previousPanel != null && currentPanel.previousPanel.isLoaded)
            {
                currentPanel.previousPanel.Show();
                currentPanel = currentPanel.previousPanel;
            }
        }

        public void DestroyPanel<T>()
        {
            UnLoadPanel(GetUrl(typeof(T)));
        }

        void UnLoadPanel(string url)
        {
            if (m_UIPanelDic.TryGetValue(url, out UIPanel panel))
            {
                panel.Destroy();
                m_UIPanelDic.Remove(url);
            }
            else
                Debug.LogError("UIPanel not exist: " + url);
        }

        void UnLoadAllPanel()
        {
            foreach(var panel in m_UIPanelDic.Values)
                panel.Destroy();
            m_UIPanelDic.Clear();
        }

        //根据UIPanel的Type获取其对应的url
        string GetUrl(Type t)
        {
            foreach (var keyPairValue in m_atrributeDataDic)
                if (keyPairValue.Value.type == t)
                    return keyPairValue.Key;
            Debug.LogError($"Cannot found type({t.Name})");
            return null;
        }

        public override void OnApplicationQuit()
        {
            UnLoadAllPanel();
        }
    }
}

然后就是我们的UIPanel了,继承于UIView

namespace Hotfix.UI
{
    public class UIPanel : UIView
    {
        //UIPanel间的自定义传递数据
        public object data;

        //前一个UIPanel,用于隐藏自己的时候,Show前者
        public UIPanel previousPanel;

        public UIPanel(string url) : base(url)
        {
        }

        public override void Destroy()
        {
            base.Destroy();
            previousPanel = null;
        }
    }
}

最后我们就可以编写我们需要的UI界面的对应UIPanel了,例如登陆界面的LoginPanel,我们会添加UIAttribute来配置其prefab的路径(demo中就简单的丢在了Resources目录下),然后在Init方法中去找到我们需要使用到的控件,show方法中可以做一些界面每次显示的时候需要的操作,数据的显示。等等

using Hotfix.Manager;
using UnityEngine.UI;

namespace Hotfix.UI
{
    [UI("LoginPanel")]
    public class LoginPanel : UIPanel
    {
        Button m_loginBtn;
        InputField m_userNameInput;

        public LoginPanel(string url) : base(url)
        {
        }

        public override void Init()
        {
            base.Init();
            m_loginBtn = transform.Find("LoginButton").GetComponent<Button>();
            m_userNameInput = transform.Find("UserNameInputField").GetComponent<InputField>();

            m_loginBtn.onClick.AddListener(OnClick);
        }

        void OnClick()
        {
            UIPanelManager.Instance.ShowPanel<MainPanel>(m_userNameInput.text);
        }
    }
}

注:由于代码量较多,剩下的代码有兴趣的还是看GitHub的工程。

由于添加了ManagerBaseWithAttr类,在HotfixLaunch类中也要进行相应的处理,同时也暂时在其中显示第一个界面

namespace Hotfix
{
    public class HotfixLaunch
    {
        static List<IManager> m_managerList = new List<IManager>();

        public static void Start(bool isHotfix)
        {
            ......

            //获取hotfix的管理类,并启动
            foreach (var t in allTypes)
            {
                try
                {
                    if (t != null && t.BaseType != null && t.BaseType.FullName != null)
                    {
                        ......
                        else if (t.BaseType.FullName.Contains(".ManagerBaseWithAttr`"))
                        {
                            Debug.Log("加载管理器-" + t);
                            var manager = t.BaseType.BaseType.GetProperty("Instance").GetValue(null, null) as IManager;
                            m_managerList.Add(manager);
                            attributeManagerList.Add(manager as IAttribute);
                            continue;
                        }
                    }
                }
                ......
            }

            //遍历所有类和ManagerBaseWithAttr管理器,找出对应的被ManagerBaseWithAttr管理的子类。例如UIPanelManager和LoginPane的关系
            foreach (var t in allTypes)
                foreach (var attr in attributeManagerList)
                    attr.CheckType(t);

            .....

            UIPanelManager.Instance.ShowPanel<LoginPanel>(null);
        }
        ......
    }
}

 


版权声明:本文为wangjiangrong原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。