在 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);
        }
        ......
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章