找回密碼
 注冊帳號

掃一掃,訪問微社區

涼鞋同學 Unity 游戲框架搭建 2018 (六) UI 管理神器 UI Kit

0
回復
303
查看
打印 上一主題 下一主題
[ 復制鏈接 ]
排名
2324
昨日變化

85

主題

535

帖子

2491

積分

Rank: 9Rank: 9Rank: 9

UID
148923
好友
69
蠻牛幣
2693
威望
0
注冊時間
2016-5-17
在線時間
849 小時
最后登錄
2020-4-28

專欄作家

QQ

馬上注冊,結交更多好友,享用更多功能,讓你輕松玩轉社區。

您需要 登錄 才可以下載或查看,沒有帳號?注冊帳號

x
UI Kit 快速入門
首先我們來進行 UI Kit 的快速入門
制作一個界面的,步驟如下:
  • 準備
  • 生成代碼
  • 邏輯編寫
  • 運行
1. 準備
  • 先創建一個場景 TestUIHomePanel。
    enter image description here
  • 刪除 Hierarchy 其他的 GameObject。
  • 搜索 UIRoot.prefab,拖入 Hierarchy。
enter image description here
  • 在 UIRoot / Design GameObject 下創建 Panel ( 右擊 Design -> UI -> Panel )。
  • 將該 Panel 改名為 UIHomePanel。
  • 添加按鈕 BtnStart、BtnAbout。
enter image description here
  • 對 BtnStart、BtnAbout 添加 UIMark Component。
  • 將 UIHomePanel 做成 prefab,再進行 AssetBundle 標記。
enter image description here2. 生成代碼
  • 右擊 UIHomePanel Prefab 選擇 @UI Ki t- Create UI Code,生成 UI 腳本。
enter image description here
  • 此時,生成的腳本自動掛到了 UIHomePanel 上,并且進行 UIMark 標記的控件,自動綁定到 prefab 上,如圖所示:
enter image description here3. 邏輯編寫
  • 打開 UIHomePanel ,在 ResgisterUIEvent 上編寫 Button 事件綁定,編寫后代碼如下:
[C#] 純文本查看 復制代碼
/* 2018.7 涼鞋的MacBook Pro (2) */

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using QFramework;

namespace QFramework.UIKitExample
{
    public class UIHomePanelData : UIPanelData
    {
        // TODO: Query Mgr's Data
    }

    public partial class UIHomePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIHomePanelData ?? new UIHomePanelData();
            //please add init code here
        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
            throw new System.NotImplementedException ();
        }

        protected override void RegisterUIEvent()
        {
            BtnStart.onClick.AddListener(() =>
            {
                Log.E("BtnStart Clicked !!!");
            });

            BtnAbout.onClick.AddListener(() =>
            {
                Log.E("BtnAbout Clicked !!!");
            });
        }

        protected override void OnShow()
        {
            base.OnShow();
        }

        protected override void OnHide()
        {
            base.OnHide();
        }

        protected override void OnClose()
        {
            base.OnClose();
        }

        void ShowLog(string content)
        {
            Debug.Log("[ UIHomePanel:]" + content);
        }
    }
}

4. 運行
  • 創建一個 空 GameObject,命名為 TestUIHomePanel,掛上 UIPanelTester 腳本。
  • UIPanelTester 腳本的 PanelName 填寫為 UIHomePanel。
enter image description here
  • 運行!
  • 運行之后,點擊對應的按鈕則會有對應的輸出。
UI Kit 的快速入門就介紹到這里。其中的 UI Kit 代碼生成和 Res Kit 的 Simulation Mode 為日常開發節省了大量的時間。
UI Kit 層級管理
我們的 UIHomePanel 是怎么打開的呢?
答案在 UIPanelTester 中。代碼如下:
[C#] 純文本查看 復制代碼
namespace QFramework
{
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    [System.Serializable]
    public class UIPanelTesterInfo
    {
        /// <summary>
        /// 頁面的名字
        /// </summary>
        public string PanelName;

        /// <summary>
        /// 層級名字
        /// </summary>
        public UILevel Level;
    }

    public class UIPanelTester : MonoBehaviour
    {
        /// <summary>
        /// 頁面的名字
        /// </summary>
        public string PanelName;

        /// <summary>
        /// 層級名字
        /// </summary>
        public UILevel Level;

        [SerializeField] private List<UIPanelTesterInfo> mOtherPanels;

        private void Awake()
        {
            ResMgr.Init();
        }

        private IEnumerator Start()
        {
            yield return new WaitForSeconds(0.2f);

            UIMgr.OpenPanel(PanelName, Level);

            mOtherPanels.ForEach(panelTesterInfo => { UIMgr.OpenPanel(panelTesterInfo.PanelName, panelTesterInfo.Level); });
        }
    }
}

在 Start 方法中,有一句 UIMgr.OpenPanel ( PanelName, Level );
其中 PanelName 是我們填寫的 UIHomePanel。Level 則是用來顯示層級的枚舉。
枚舉定義如下:
[C#] 純文本查看 復制代碼
    public enum UILevel
    {
        AlwayBottom        = -3, //如果不想區分太復雜那最底層的UI請使用這個
        Bg                 = -2, //背景層 UI
        AnimationUnder     = -1, //動畫層
        Common             = 0, //普通層 UI
        AnimationOn        = 1, // 動畫層
        PopUI              = 2, //彈出層 UI
        Guide              = 3, //新手引導層
        Const              = 4, //持續存在層 UI
        Toast              = 5, //對話框層 UI
        Forward            = 6, //最高UI層用來放置UI特效和模型
        AlwayTop           = 7, //如果不想區分太復雜那最上層的UI請使用這個
    }

默認為 UILevel.Common。
層級管理的核心實現比較簡單,代碼如下:
[C#] 純文本查看 復制代碼
            switch (uiLevel)
            {
                case UILevel.Bg:
                    ui.Transform.SetParent(mBgTrans);
                    break;
                case UILevel.AnimationUnderPage:
                    ui.Transform.SetParent(mAnimationUnderTrans);
                    break;
                case UILevel.Common:
                    ui.Transform.SetParent(mCommonTrans);
                    break;
                case UILevel.AnimationOnPage:
                    ui.Transform.SetParent(mAnimationOnTrans);
                    break;
                case UILevel.PopUI:
                    ui.Transform.SetParent(mPopUITrans);
                    break;
                case UILevel.Const:
                    ui.Transform.SetParent(mConstTrans);
                    break;
                case UILevel.Toast:
                    ui.Transform.SetParent(mToastTrans);
                    break;
                case UILevel.Forward:
                    ui.Transform.SetParent(mForwardTrans);
                    break;
            }

這種是最通用最常見的層級管理方式。只要將 UIPanel 放到對應層級的 GameObject 下面就好了。
不過這種層級管理會有一點問題。當這個 UIRoot 只有一個 Canvas 的時候,頁面的打開或關閉都會進行 Canvas 網格的重新排序,也就是網格重建,所以很多方案都建議,每個 UIPanel 掛一個 Canvas。然后根據 Canvas Sorting Order 進行層級管理。
使用 Canvas 的 Sorting Order 可以更精細地進行層級管理,而 UI Kit 目前的方式相對粗糙一些。
使用 Canvas 的方式是 UI Kit 的一個方向,等 QFramework 團隊找到比較簡潔的實現之前,UI Kit 還是默認使用當前方式。
UI Kit 最佳實踐 (一) 界面跳轉及數據展示
接下來,我們來制作兩個界面。一個是 UILevelPanel,一個是 UIGamePanel。
UILevelPanel 功能定義:
  • 有若干個關卡,這里暫時定義為九個關卡。
  • 點擊任意關卡則跳轉到 UIGamePanel,并在 UIGamePanel 中顯示當前關卡信息。比如,在 UILevelPanel 點擊第一關,則打開的 UIGamePanel 則展示 第一關,同理第二關。
UIGamePanel 功能定義:
  • 游戲暫停按鈕。
    關卡信息展示,這里只展示第幾關就夠了。            

這里 UILevelPanel 和 UIGamePanel 的制作的詳細過程和 UI Kit 快速入門類似。簡單展示下兩個頁面的布局,和相關的邏輯代碼。
UIGamePanel.prefab:
enter image description here
  • BtnPause 是一個 Button。
  • Level 是一個 Text,主要是用來表示第幾關的。
其代碼如下:
UIGamePanel.cs
[C#] 純文本查看 復制代碼
/* 2018.7 涼鞋的MacBook Pro (2) */

namespace QFramework.Example
{
    public class UIGamePanelData : UIPanelData
    {
        public int LevelIndex = 1;
    }

    public partial class UIGamePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIGamePanelData ?? new UIGamePanelData();
            //please add init code here

            Level.text = "第" + mData.LevelIndex + "關";
        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
            throw new System.NotImplementedException ();
        }

        protected override void RegisterUIEvent()
        {
            BtnPause.onClick.AddListener(() =>
            {
                Log.E("BtnPause Clicked");
            });
        }
    }
}

在 InitUI 方法中,會接收到一個 UIData,然后轉為 UIGamePanelData 后賦值給 MData。
那么這個 UIData 從哪里傳過來呢?
答案是 UIMgr.Open 這個 API,這個 API 提供傳入相應界面的 UIData。這部分在下一小結詳細說。
現在重點是,在 UIMgr.Open 不傳入 UIData 時,UIGamePanel, UIData 有可能是空的,那么當 UIData 為空時,則會默認創建一個 UIGamePanelData()。
默認創建一個 UIGamePanelData 這樣做有什么好處呢?
答案是 方便測試,可以在 UIGamePanelData 里給數據設置一些默認值,這里 LevelIndex 默認是 1。當然這也不是本小節的重點,這個也下一小節詳細說。
由于可以設置默認值,所以 TestUIGamePanel.scene 運行之后如下:
enter image description here
默認就是第一關。這樣負責編寫 UIGamePanel 和游戲戰斗模塊的同學們,主要改自己部分的 UIGamePanelData 就可以進行一些測試了,而不是從頭開始運行游戲手點直到打開 UIGamePanel 才看到測試結果,重點是 UIGamePanel 不依賴任何其他數據,只是作為數據的一個展示而已。反正這不是本節重點。。。不好意思實在忍不住:)
接下來 UILevelPanel
UILevelPanel.prefab:
enter image description here
  • LevelProtoype 是一個 Button,代表關卡 item 的原型。
  • LevelsContainer 則是一個 普通的 GameObject,不過掛上了 GridLayoutGroup 了。
  • BtnBack 則是左上角的的返回按鈕。
UILevelPanel 可能會復雜一點了,代碼如下:
[C#] 純文本查看 復制代碼
/* 2018.7 涼鞋的MacBook Pro (2) */

using UnityEngine.UI;

namespace QFramework.Example
{
    public class UILevelPanelData : UIPanelData
    {
        // TODO: Query Mgr's Data
    }

    public partial class UILevelPanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UILevelPanelData ?? new UILevelPanelData();
            //please add init code here

            for (var i = 0; i < 10; i++)
            {

                LevelPrototype.Instantiate()
                        .Parent(LevelsContainer)
                        .LocalScaleIdentity()
                        .LocalPositionIdentity()
                        .Show()
                        .ApplySelfTo(btnLevel=>
                        {
                            var levelIndex = i + 1;

                            btnLevel.onClick.AddListener(() =>
                            {
                                CloseSelf();
                                UIMgr.OpenPanel<UIGamePanel>(uiData: new UIGamePanelData()
                                {
                                    LevelIndex = levelIndex
                                });
                            });
                        })
                        .transform.Find("Text").GetComponent<Text>().text = "第" + (i + 1) + "關";
            }

        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
            throw new System.NotImplementedException ();
        }

        protected override void RegisterUIEvent()
        {
            BtnBack.onClick.AddListener(() =>
            {
                CloseSelf();
                UIMgr.OpenPanel<UIMainPanel>();
            });
        }
    }
}

核心代碼:
[C#] 純文本查看 復制代碼
for (var i = 0; i < 10; i++)
{

    LevelPrototype.Instantiate()     // var BtnLevel = GameObject.Instantiate(LevelPrototype)
            .Parent(LevelsContainer) // BtnLevel.transform.SetParent(LevelsContainer)
            .LocalScaleIdentity()    // BtnLevel.localScale = Vector3.one
            .LocalPositionIdentity() // BtnLevel.localPosition = Vector3.zero
            .Show()                 // BtnLevel.gameObject.SetActive(true)
            .ApplySelfTo(btnLevel=>  // 這里就是為了捕獲上邊的 BtnLevel 然后進行一些操作
            {
                var levelIndex = i + 1;

                btnLevel.onClick.AddListener(() =>
                {
                    CloseSelf();
                    UIMgr.OpenPanel<UIGamePanel>(uiData: new UIGamePanelData()
                    {
                        LevelIndex = levelIndex
                    });
                });
            })
            .transform.Find("Text").GetComponent<Text>().text = "第" + (i + 1) + "關";
}

請仔細月底以上注釋部分。
總之,注釋部分則是對鏈式支持的解釋。不是本文重點。
重點是:
[C#] 純文本查看 復制代碼
UIMgr.OpenPanel<UIGamePanel>(uiData: new UIGamePanelData()
{
    LevelIndex = levelIndex
});

打開 UIGamePanel 的時候,可以傳入一個 Data 進去。而這個 Data 就是 UIGamePanel 中 InitUI 接收到的 uiData。非常簡單。
運行結果如下:
enter image description here
功能完成。
UIPanel 之間的數據傳遞
由上一小節介紹的從 UILevelPanel 傳數據到 UIGamePanel 這種傳遞數據的方式有什么好處呢?
首先在筆者的框架里,推薦將 UI 界面當做只是進行數據展示的工具。是一個 Input 和 Output 模塊。輸入數據,然后將數據展示到界面上。在筆者接觸的很多項目中,在界面邏輯中添加了非常多數據訪問相關的代碼,各種 DataManager、ConfigManager 等等,這里是非常不推薦的。推薦所有要展示的初始數據,都是通過 uiData 傳入進來,這要每個界面或模塊都可以進行獨立測試。
獨立測試、團隊成員之間互不干擾、并且可以自己寫一些測試數據這僅僅是第一個好處,還不夠。
第二個好處則是為了方便完成 UIPanelStack 功能,這個功能是主要完成一個 返回上一頁這個功能。舉個例子。
假如 UIGamePanel 可以從 UILevelPanel 打開,那么 UILevelPanel 打開的 UIGamePanel 返回上一頁應該是 UILevelPanel。
同理,通過 UIOtherPanel 打開的 UIGamePanel 返回的上一個頁面應該是 UIOtherPanel。
這個功能如果用一堆判斷 + UIGamePanelData 來實現很容易,但是代碼不是很優雅,假如 UIGamePanel 可以從十多個頁面打開,那是不是要寫十多個條件判斷? 而且要在每個打開 UIGamePanel 的地方都要把 上一個頁面信息記錄到 UIGamePanelData 里。這樣很麻煩。要完成這個最簡單的還是一個 UIPanelStack,用一個堆棧來記錄頁面打開關閉的信息。
有了  UIPanelData 這個概念,那么很容易就可以完成這個功能。主要抽象出一個 UIPanelInfo 就好了,
[C#] 純文本查看 復制代碼
namespace QFramework
{
    public class UIPanelInfo
    {
        public IUIData UIData;

        public UILevel Level;

        public string AssetBundleName;

        public string PanelName;
    }
}

這是第二個好處了,有了 UIPanelData 這個基礎就可以做更多的事情。UIPanelStack 功能不是本小節重點,在接下來會詳細進行介紹。
總結下來好處就是兩點:
  • 獨立測試、團隊成員之間互不干擾、并且可以自己寫一些測試數據。
  • 更容易實現 UIPanelStack。
不過,這個 UIPanelData 的設計還有改進的空間,如果將 UIPanelData 做成可序列化的,并且 mData 是一個可賦值的話,那么做界面測試的時候就不用更改代碼了,只在 prefab 上就可以進行測試數據的修改了。可以節省大量的代碼編譯時間。這個功能在筆者自己的項目進行過實驗,是可以實現的,等穩定了會在 UI Kit 未來的版本集成。
UIMgr/UIManager
UIMgr 本身是個 Manager,之前在 ResKit 有介紹過,只要叫 XXXMgr 的本身就是一個容器,是一個對外提供容器元素一系列操作的容器。而 UIMgr 則是 UI 的容器,里邊維護了一個字典,Dictionary 。所有的 UIMgr.Open/Close/Get 等,都是先進行一次字典查詢,再進行相應操作的。這個不難理解,大部分市面上的方案和 UI Kit 類似,都比較成熟了,這里不值得浪費篇幅。
UIPanelStack 管理
UIPanelStack 也就是堆棧,一般只是用來完成返回上一頁這個功能的。在沒用堆棧管理頁面信息之前,都是打開一個頁面,然后在打開的頁面上記錄上一頁面的信息,然后當頁面點擊返回時,再根據記錄的上一個頁面的信息打開上一個頁面。這種實現在頁面數量不多、跳轉邏輯不復雜的情況下可以勉強應付。但是跳轉邏輯比較復雜的情況下還是要實現一個,所以就添加了 UIPanelStack 這個功能。
起初的實現是所有的頁面,每當關閉時頁面信息全部壓到 Stack 里。這里的頁面信息定義如下:
[C#] 純文本查看 復制代碼
    public class UIPanelInfo
    {
        public IUIData UIData;

        public UILevel Level;

        public string AssetBundleName;

        public string PanelName;
    }

簡單介紹下:
  • UIData 是 在關閉時頁面的數據快照
  • Level 則是關閉時所在的層級
  • AssetBundleName 則是所加載的 AB 名,如果沒有傳入則為空。
  • PanelName 則是打開頁面時傳入的頁面名字。
后來發現筆者參與的項目不需要每個頁面都壓入棧中,只是少數的幾個頁面需要。
所以提供了兩個手動的 API。
[C#] 純文本查看 復制代碼
UIMgr.Push<T>/UIMgr.Push(string panelName);
UIMgr.Back(IUIPanel panel)/UIMgr.Back(string panelName);

Push 很容易理解,就是 UIPanel 的壓棧操作。Back 則是,返回到最近 Push 進棧中的頁面。
實現原理也很簡單。
  • Push 時將傳入的 panelName 對應的 Panel 關閉掉,生成 UIPanelInfo 后,將 UIPanelInfo 壓入到棧中。
  • Back 則是將傳入的 panelName 對應的 Panel 關閉掉,從 棧中彈出一個 UIPanelInfo,之后根據信息打開頁面并傳入 Info 中的數據。
核心代碼:
UIManager.cs
[C#] 純文本查看 復制代碼
        public void Push(IUIPanel view)
        {
            if (view != null)
            {
                mUIStack.Push(view.PanelInfo);
                view.Close();
                mAllUI.Remove(view.Transform.name);
            }
        }

        public void Back(string currentPanelName)
        {
            var previousPanelInfo = mUIStack.Pop();
            CloseUI(currentPanelName);
            OpenUI(previousPanelInfo.PanelName, previousPanelInfo.Level, previousPanelInfo.UIData,
                previousPanelInfo.AssetBundleName);
        }

關于 UIPanelStack 介紹到這里。
這個功能目前還比較簡陋,在 QFramework 文檔中還沒有進行介紹。不過相比把所有的頁面信息都進行壓棧操作,按需手動的這種方式更合適一些。
小結 (一)
事實上,獨立測試的界面完全替代了 QFramework 初期所提供的 QApp 模塊化的方式。這個不理解沒關系,每個界面的開發已經是模塊化了,不過只是從業務進行橫向的模塊化。已經夠用了,畢竟大部分的項目都是進行 UI 界面的制作和修改。其他一些戰斗系統等等,比較大的模塊,也多少會依賴于 UI 部分,那么這種的推薦用一個 UI 界面進行一個模塊的入口。總之在業務邏輯以及界面角度來看,已經支持了模塊化的架構。
UI Kit 最佳實踐 (二) 暫停界面與通信
接下來我們接著進行 UI Kit 最佳實踐,之前我們完成了 UILevelPanel 和 UIGamePanel。
而 UIGamePanel 中的暫停功能還沒有完成。點擊暫停功能則應該打開暫停界面。
所以我們先完成暫停界面的制作:
UIGamePausePanel.prefab:
enter image description here
  • Image: 是對話框的白色背景
  • BtnContinue:  是繼續按鈕
  • BtnReplay:  是重玩按鈕
  • BtnMain:  是主頁按鈕
其代碼如下:
[C#] 純文本查看 復制代碼
/* 2018.7 涼鞋的MacBook Pro (2) */

namespace QFramework.Example
{
    public class UIGamePausePanelData : UIPanelData
    {
        // TODO: Query Mgr's Data
    }

    public partial class UIGamePausePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIGamePausePanelData ?? new UIGamePausePanelData();
            //please add init code here
        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
            throw new System.NotImplementedException ();
        }

        protected override void RegisterUIEvent()
        {
            BtnMain.onClick.AddListener(() =>
            {
                CloseSelf();
                UIMgr.ClosePanel<UIGamePanel>();
                UIMgr.OpenPanel<UIMainPanel>();
            });

            BtnReplay.onClick.AddListener(() =>
            {
                Log.E("BtnPlay Clicked");
            });

            BtnContinue.onClick.AddListener(() =>
            {
                CloseSelf();
            });
        }
    }
}

代碼比較簡單,主要是三個按鈕的事件注冊。
而 UIGamePanel.cs 進行暫停按鈕的注冊:
/* 2018.7 涼鞋的MacBook Pro (2) */
namespace QFramework.Example
{
    public class UIGamePanelData : UIPanelData
    {
        public int LevelIndex = 1;
    }   

    public partial class UIGamePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIGamePanelData ?? new UIGamePanelData();
            //please add init code here

            Level.text = "第" + mData.LevelIndex + "關";         
        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
        }

        protected override void RegisterUIEvent()
        {
            BtnPause.onClick.AddListener(() =>
            {
                UIMgr.OpenPanel<UIGamePausePanel>(UILevel.PopUI);
            });
        }
    }
}

[C#] 純文本查看 復制代碼

原來的 BtnPause 點擊之后進行一些日志的輸出,現在則改為了打開 UIGamePausePanel 頁面。
這里層級為 UILevel.PopUI。
這樣一個打開暫停頁面的功能就完成了。
結果如下:
enter image description here
但是一般的暫停頁面沒有這么簡單。
當打開暫停頁面的時候,正在進行的游戲應該全部暫停。這里最容易的實現就是 Time.timeScale = 0.0f;
不過這不是重點,當繼續游戲時候,還要通知 UIGamePanel 或者對應的 GameManager 進行暫停的恢復。
這就涉及到了對象之間通信的問題。
最簡單的方式就是,在 UIGamePausePanel 中獲取 UIGamePanel ,然后進行相應恢復操作。
UIGamePanel.cs 先增加幾個方法:
[C#] 純文本查看 復制代碼
/* 2018.7 涼鞋的MacBook Pro (2) */

namespace QFramework.Example
{
    public class UIGamePanelData : UIPanelData
    {
        public int LevelIndex = 1;
    }   

    public partial class UIGamePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIGamePanelData ?? new UIGamePanelData();
            //please add init code here

            Level.text = "第" + mData.LevelIndex + "關";
        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
        }

        protected override void RegisterUIEvent()
        {
            BtnPause.onClick.AddListener(() =>
            {
                GamePause();
                UIMgr.OpenPanel<UIGamePausePanel>(UILevel.PopUI);
            });
        }


        void GameStart()
        {

        }

        void GamePause()
        {
            Log.E("GamePause");
        }

        public void GameResume()
        {
            Log.E("GameResume");
        }

        void GameOver()
        {

        }

    }
}

由于 GameResume 需要交給 UIGamePausePanel 調用。所以要做成 public 方法。
/* 2018.7 涼鞋的MacBook Pro (2) */

namespace QFramework.Example
{
    public class UIGamePausePanelData : UIPanelData
    {
        // TODO: Query Mgr's Data
    }

    public partial class UIGamePausePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIGamePausePanelData ?? new UIGamePausePanelData();
            //please add init code here
        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
            throw new System.NotImplementedException ();
        }

        protected override void RegisterUIEvent()
        {
            BtnMain.onClick.AddListener(() =>
            {
                CloseSelf();
                UIMgr.ClosePanel<UIGamePanel>();
                UIMgr.OpenPanel<UIMainPanel>();
            });

            BtnReplay.onClick.AddListener(() =>
            {
                Log.E("BtnPlay Clicked");
            });

            BtnContinue.onClick.AddListener(() =>
            {
                CloseSelf();
                UIMgr.GetPanel<UIGamePanel>().GameResume();
            });
        }
    }
}
在 BtnContinue 中獲取,這是最簡單的實現。
但是從子 Panel 對象獲取 父 Panel 對象,不太優雅。
這里筆者介紹一個原則:
  • 自底向上,發消息或提供委托。
  • 自頂向下,直接緩存引用。
  • 同級之間,發消息。
應用到當前的例子,則為 子 Panel 應該對 父 Panel 提供委托或者發消息。
我們先實現提供委托的方式。
這個比較簡單,由于 BtnContinue 其實是 public 的,所以在 UIGamePanel 打開 UIGamePausePanel 的同時就可以進行一個按鈕點擊事件的注冊。
代碼如下:
/
[C#] 純文本查看 復制代碼
* 2018.7 涼鞋的MacBook Pro (2) */

namespace QFramework.Example
{
    public class UIGamePanelData : UIPanelData
    {
        public int LevelIndex = 1;
    }

    public partial class UIGamePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIGamePanelData ?? new UIGamePanelData();
            //please add init code here

            Level.text = "第" + mData.LevelIndex + "關";          
        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
        }

        protected override void RegisterUIEvent()
        {
            BtnPause.onClick.AddListener(() =>
            {
                GamePause();
                UIMgr.OpenPanel<UIGamePausePanel>(UILevel.PopUI).BtnContinue.onClick.AddListener(GameResume);
            });
        }

        void GameStart()
        {

        }

        void GamePause()
        {
            Log.E("GamePause");
        }

        void GameResume()
        {
            Log.E("GameResume");
        }

        void GameOver()
        {

        }

    }
}

代碼很簡單,這要在 BtnPause 按鈕點擊時,進行 BtnContinue 的點擊事件注冊。
Button 的 onClick 事件,不用擔心會被覆蓋,因為用的是 AddListener 這個 API,是增加,實質上類似于 += 操作。這樣就可以把事件從外部注入到 UIGamePausePanel 中。
這也是比較方便的一種方式,尤其是在 Dialog 或者 Prompt 通用的時候,其標記過的控件都可以在外邊訪問。
這樣有利有弊,有點不符合面向對象的設計原則,不過問題不大。
這樣第二種方式就完成了。
第三種方式則是通過發送消息,UIGamePausePanel 可以把事件發送給 UIGamePanel。
首先要在 UIGamePanel 中進行事件的定義:
[C#] 純文本查看 復制代碼
    public enum UIGamePanelEvent
    {
        Start = QMgrID.UI,
        GameResume,
        End
    }

事件的注冊:
    public partial class UIGamePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIGamePanelData ?? new UIGamePanelData();
            //please add init code here

            Level.text = "第" + mData.LevelIndex + "關";

            RegisterEvent(UIGamePanelEvent.GameResume);
        }

        ...
    }
事件的處理:
[C#] 純文本查看 復制代碼
   public partial class UIGamePanel : UIPanel
    {
        ...

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
            if (eventId == (int) UIGamePanelEvent.GameResume)
            {
                GameResume();
            }
        }

        ...
    }

接收消息之后,馬上調用 GameResume 方法就好了。
完整 UIGamePanel.cs 代碼如下:
[C#] 純文本查看 復制代碼
/* 2018.7 涼鞋的MacBook Pro (2) */

namespace QFramework.Example
{
    public class UIGamePanelData : UIPanelData
    {
        public int LevelIndex = 1;
    }

    public enum UIGamePanelEvent
    {
        Start = QMgrID.UI,
        GameResume,
        End
    }


    public partial class UIGamePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIGamePanelData ?? new UIGamePanelData();
            //please add init code here

            Level.text = "第" + mData.LevelIndex + "關";

            RegisterEvent(UIGamePanelEvent.GameResume);
        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
            if (eventId == (int) UIGamePanelEvent.GameResume)
            {
                GameResume();
            }
        }

        protected override void RegisterUIEvent()
        {
            BtnPause.onClick.AddListener(() =>
            {
                GamePause();
                UIMgr.OpenPanel<UIGamePausePanel>(UILevel.PopUI);
            });
        }

        void GameStart()
        {

        }

        void GamePause()
        {
            Log.E("GamePause");
        }

        void GameResume()
        {
            Log.E("GameResume");
        }

        void GameOver()
        {

        }

    }
}

接下來是在 UIGamePausePanel 中發送事件的部分。
非常簡單,代碼如下:
[C#] 純文本查看 復制代碼
/* 2018.7 涼鞋的MacBook Pro (2) */

namespace QFramework.Example
{
    public class UIGamePausePanelData : UIPanelData
    {
        // TODO: Query Mgr's Data
    }

    public partial class UIGamePausePanel : UIPanel
    {
        protected override void InitUI(IUIData uiData = null)
        {
            mData = uiData as UIGamePausePanelData ?? new UIGamePausePanelData();
            //please add init code here
        }

        protected override void ProcessMsg (int eventId,QMsg msg)
        {
            throw new System.NotImplementedException ();
        }

        protected override void RegisterUIEvent()
        {
            BtnMain.onClick.AddListener(() =>
            {
                CloseSelf();
                UIMgr.ClosePanel<UIGamePanel>();
                UIMgr.OpenPanel<UIMainPanel>();
            });

            BtnReplay.onClick.AddListener(() =>
            {
                Log.E("BtnPlay Clicked");
            });

            BtnContinue.onClick.AddListener(() =>
            {
                SendEvent(UIGamePanelEvent.GameResume);
                CloseSelf();
            });
        }
    }
}

在 BtnContinue 按鈕點擊之后進行事件的發送就好了。
這就是事件機制的方式。
目前有三種方式:
  • 獲取父 Panel 調用方法。
  • 對父 Panel 提供委托。
  • 發送消息。
這里筆者比較推薦使用第二種和第三種。
其中最推薦的還是,第二種,提供委托的方式。這樣事件能夠很好地進行接收,保持單向的引用,可以通過代碼了解兩者之間的關系,所以這通常是很緊實的設計。
而第三種雖然推薦,但是多少會有一些風險。可能造成消息滿天飛的情況,萬不得已的情況下慎用,唯一的好處就是松耦合。在跨模塊之間通信,或者同級的對象之間建議用消息,這種情況已經是萬不得已的情況下了。
而第一種方式呢,造成了雙向引用,是完全違背常識的,所以不推薦,不過項目緊的時候先完成需求為先。
ok,三種方式的利弊介紹到這里。
觀察者模式與事件機制
觀察者模式是對象/模塊之間解耦的利器
觀察者模式是什么?筆者看了很多官方和書上定義后也是一頭霧水,我們先拋棄 Observer/Subscriber 等技術概念,直接來段簡單易讀的代碼更容易理解些。
[C#] 純文本查看 復制代碼
    class Person
    {
        public string Name;

        public void Say(string msg)
        {
            Log.E ("Person:{0} Say:{1}", Name, msg);
        }

        public void ReceiveMsg(string msg)
        {
            Say (msg);
        }
    }

代碼不難理解,主要是 ReceiveMsg 方法用來接收消息。
  
[C#] 純文本查看 復制代碼
  class Me
    {
        List<Person> mContacts = new List<Person>();

        public void RegisterContact(Person person)
        {
            mContacts.Add(person);
        }

        public void SendMsgToContancts()
        {
            mContacts.ForEach(person => person.ReceiveMsg("Hi"));
        }
    }

Me  就是,也就是發送者,將消息發送出去。
  • mContacts,就是聯系人的意思。
  • RegisterContact,需要在我的聯系人名單里注冊好 Person,Person 才能接收到我發出去的消息。
  • SendMsgToContacts,則是我向聯系人發送消息。
測試代碼:
[C#] 純文本查看 復制代碼
   public class ObserverPatternExample : MonoBehaviour 
    {
        // Use this for initialization
        IEnumerator Start () 
        {
            Me me = new Me ();

            me.RegisterPerson(new Person (){ Name = "張三" });
            me.RegisterPerson(new Person (){ Name = "李四" });
            me.RegisterPerson(new Person (){ Name = "王五 "});

            yield return new WaitForSeconds (1.0f);

            me.SendMsgToContancts ();
        }
    }

輸出結果為:
[C#] 純文本查看 復制代碼
Person:張三 Say:Hi
Person:李四 Say:Hi
Person:王五 Say:Hi

這是一種一對多的消息廣播。
當然多對一,多對多很容易實現,原理都是通過觀察者模式。
有了以上例子為基礎,再去看設計模式中的觀察者模式就會更簡單了。
觀察者模式的經典實現,都有一個消息注冊列表,一般是在,Subject 中,這個 Subject 對應的是  以上例子中的 Me,而 Observer 則對應的是 Person。
接下來介紹下觀察者模式的應用,消息機制 QEventSystem。
QEventSystem 提供了如下的 API:
[C#] 純文本查看 復制代碼
QEventSystem.RegisterEvent(eventKey,onEventCallback);
QEventSystem.UnRegisterEvent(eventKey,onEventCallback);
QEventSystem.SendEvent(eventKey,params object[] args);

很簡單,就是注冊,注銷,和發送事件。
而 QEventSystem 本身就是一個消息注冊列表,相當于上個例子中的 List<Person\> 封裝到了 QEventSystem。
所以如果將以上例子改成使用 QEventSystem,則代碼會變成如下:
[C#] 純文本查看 復制代碼
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using QFramework;

namespace CourseExample
{
    class Person
    {
        public const int PersonEvent = 0;

        public string Name;

        public Person()
        {
            QEventSystem.RegisterEvent (PersonEvent, OnReceiveEvent);
        }

        void OnReceiveEvent(int eventKey,object[] args)
        {
            var receivedMsg = args [0] as string;
            Say (receivedMsg);
        }

        public void Say(string msg)
        {
            Log.E ("Person:{0} Say:{1}", Name, msg);
        }
    }

    class Me
    {
        public void SendMsgToContancts()
        {
            QEventSystem.SendEvent (Person.PersonEvent, "Hi");
        }
    }

    public class ObserverPatternExample : MonoBehaviour 
    {
        // Use this for initialization
        IEnumerator Start () 
        {
            Me me = new Me ();
            new Person (){ Name = "張三" };
            new Person (){ Name = "李四" };
            new Person (){ Name = "王五 "};

            yield return new WaitForSeconds (1.0f);

            me.SendMsgToContancts ();
        }
    }
}

QEventSystem 與觀察者模式就介紹到這里。
中介者模式與事件機制
這里不多說,主要比較下觀察者模式和中介者模式就理解了。
觀察者 (observer)模式
  • 通過 訂閱 - 發布 (subscribe-publish) 模型,消除組件之間雙向依賴
  • 消息的 發布者 (subject) 不需要知道 觀察者 (observer) 的存在
  • 兩者只需要約定消息的格式(如何訂閱、如何發布),就可以通信
  • 對應的是以上的不用 QEventSystem 的 Person/Me 的例子。
中介者 (mediator) 模式
  • 通過設置 消息中心 (message center),避免組件之間直接依賴
  • 所有的 協同者 (colleague) 只能通過 中介者 (mediator) 進行通信,
    而相互之間不知道彼此的存在
  • 當各個組件的消息出現循環時,消息中心可以消除組件之間的依賴混亂
  • 對應的是以上的使用 QEventSystem 的 Person/Me 的例子。
所以 QEventSystem 與其說是觀察者模式,不如說是中介者模式的實現。
以上關于設計模式的描述,僅僅是表達個人的理解,方便初學者更容易理解。
小結 (二)
在這個小結簡單了解了 觀察者模式和中介者模式,以及 UI Kit 所支持的事件機制。
Manager Of Managers 簡介
UI Kit 內置了一個 Manager Of Managers 的支持。
主要提供兩個功能:
  • 模塊化的腳本管理
  • 模塊化的消息系統
而模塊化的腳本管理,對我們來說不是很陌生。其中 UIMgr 與 UIPanel 則是其一種實現。
UIMgr 為腳本管理器,而 UIPanel 則是腳本單元。
對應的可以推測出, CharacterManager 則有對應的 Character,Enemy/Weapon Manager 有對應的 Weapon 和 Manager 腳本。這是在一個業務邏輯上劃分模塊的一種方式。不難理解。
UI Kit 提供了兩個基類:
QMonoBehaviour 和 QMgrBehaviour:
  • QMonoBehaviour 則是腳本的基類,比如 UIPanel 則是繼承了 QMonoBehaivour。
  • QMgrBhavriour 則是管理類的基類,比如 UIMgr 則是繼承了 QMgrBehaviour。
不難理解,而 UIMgr 里邊維護了一個關于 UIPanel 的容器。很好地表明了 QMgrBehaviour 與 QMonoBehaviour   之間的關系。
從而想實現其他模塊也變得容易,比如 CharacterManager 只要繼承 QMgrBehaviour 而 Character 腳本則繼承 QMonoBehaviour 就可以實現角色模塊了。
以上則是模塊化的腳本管理的介紹。
另一個利器則是模塊化的消息系統。
大家可能發下  UIGamePanel 與 UIGamePausePanel 實現的事件機制 與 后來的 QEventSystem 有一點區別。
在 UIGamePanelEvent 中,事件的定義如下:
[C#] 純文本查看 復制代碼
   public enum UIGamePanelEvent
    {
        Start = QMgrID.UI,
        GameResume,
        End
    }

為什么 Start 要等于 QMgrID.UI 呢?
我們先來看看 QMgrID 是什么?
[C#] 純文本查看 復制代碼
   public class QMsgSpan
    {
        public const int Count = 3000;
    }

    public partial class QMgrID
    {
        public const int Framework = 0;
        public const int UI = Framework + QMsgSpan.Count; // 3000
        public const int Audio = UI + QMsgSpan.Count; // 6000
        public const int Network = Audio + QMsgSpan.Count;
        public const int UIFilter = Network + QMsgSpan.Count;
        public const int Game = UIFilter + QMsgSpan.Count;
        public const int PCConnectMobile = Game + QMsgSpan.Count;
        public const int FrameworkEnded = PCConnectMobile + QMsgSpan.Count;
        public const int FrameworkMsgModuleCount = 7;
    }
}

是一個長度為 3000 的區間,那么從 3000 ~ 5999 則是 UI 模塊的消息。
而 6000 ~ 8999 之間的則是 Audio 消息。不難理解。
這樣就實現了一個模塊化的消息頻段。
UIGamePausePanel 發送消息的順序為:
UIGamePausePanel -> UIMgr -> UIGamePanel。
這里還沒有介紹 UIMgr 為什么可以中轉消息。
我們看了 QMgrBehaviour 與 QMonoBehaivour 實現就知道了:
QMgrBehaviour.cs
[C#] 純文本查看 復制代碼
/* Copyright (c) 2018.3 liangxie */

namespace QFramework 
{
    using System;
    using System.Collections.Generic;

    /// <summary>
    /// manager基類
    /// </summary>
    public abstract class QMgrBehaviour : QMonoBehaviour,IManager
    {
        private readonly QEventSystem mEventSystem = NonPublicObjectPool<QEventSystem>.Instance.Allocate();

        #region IManager
        public virtual void Init() {}
        #endregion

        protected int mMgrId = 0;

        protected abstract void SetupMgrId ();

        protected override void SetupMgr ()
        {
            mCurMgr = this;
        }

        protected QMgrBehaviour() 
        {
            SetupMgrId ();
        }

        public void RegisterEvents<T>(IEnumerable<T> eventIds,OnEvent process) where T: IConvertible
        {
            foreach (var eventId in eventIds)
            {
                RegisterEvent(eventId,process);
            }
        }

        public void RegisterEvent<T>(T msgId,OnEvent process) where T:IConvertible
        {
            mEventSystem.Register (msgId, process);
        }

        public void UnRegisterEvents(List<ushort> msgs,OnEvent process)
        {
            for (int i = 0;i < msgs.Count;i++)
            {
                UnRegistEvent(msgs[i],process);
            }
        }

        public void UnRegistEvent(int msgEvent,OnEvent process)
        {
            mEventSystem.UnRegister (msgEvent, process);
        }

        public override void SendMsg(QMsg msg)
        {
            if (msg.ManagerID == mMgrId)
            {
                Process(msg.EventID, msg);
            }
            else 
            {
                QMsgCenter.Instance.SendMsg (msg);
            }
        }

        public override void SendEvent<T>(T eventId)
        {
            SendMsg(QMsg.Allocate(eventId));
        }

        // 來了消息以后,通知整個消息鏈
        protected override void ProcessMsg(int eventId,QMsg msg)
        {
            mEventSystem.Send(msg.EventID,msg);
        }
    }
}

QMonoBehaviour.cs
[C#] 純文本查看 復制代碼
/* Copyright (c) 2017 liangxie */

namespace QFramework 
{
    using UnityEngine;
    using System;
    using System.Collections.Generic;

    public abstract class QMonoBehaviour : MonoBehaviour
    {
        protected bool mReceiveMsgOnlyObjActive = true;

        public void Process (int eventId, params object[] param)  
        {
            if (mReceiveMsgOnlyObjActive && gameObject.activeInHierarchy || !mReceiveMsgOnlyObjActive)
            {
                QMsg msg = param[0] as QMsg;
                ProcessMsg(eventId, msg);
                msg.Processed = true;

                if (msg.ReuseAble)
                {
                    msg.Recycle2Cache();
                }
            }
        }

        protected virtual void ProcessMsg (int eventId,QMsg msg) {}

        protected abstract void SetupMgr ();

        private QMgrBehaviour mPrivateMgr = null;

        protected QMgrBehaviour mCurMgr 
        {
            get 
            {
                if (mPrivateMgr == null ) 
                {
                    SetupMgr ();
                }

                if (mPrivateMgr == null) 
                {
                    Debug.LogError ("not set mgr yet");
                }

                return mPrivateMgr;
            }

            set { mPrivateMgr = value; }
        }

        public virtual void Show()
        {
            gameObject.SetActive (true);

            OnShow ();
        }

        protected virtual void OnShow() {}

        public virtual void Hide()
        {
            OnHide ();

            gameObject.SetActive (false);
            Log.I("On Hide:{0}",name);
        }

        protected virtual void OnHide() {}

        protected void RegisterEvents<T>(params T[] eventIDs) where T : IConvertible
        {
            foreach (var eventId in eventIDs)
            {
                RegisterEvent(eventId);
            }
        }

        protected void RegisterEvent<T>(T eventId) where T : IConvertible
        {
            mEventIds.Add(eventId.ToUInt16(null));
            mCurMgr.RegisterEvent(eventId, Process);
        }

        protected void UnRegisterEvent<T>(T eventId) where T : IConvertible
        {
            mEventIds.Remove(eventId.ToUInt16(null));
            mCurMgr.UnRegistEvent(eventId.ToInt32(null), Process);
        }

        protected void UnRegisterAllEvent()
        {
            if (null != mPrivateEventIds)
            {
                mCurMgr.UnRegisterEvents(mEventIds, Process);
            }
        }

        public virtual void SendMsg(QMsg msg)
        {
            mCurMgr.SendMsg(msg);
        }

        public virtual void SendEvent<T>(T eventId) where T : IConvertible
        {
            mCurMgr.SendEvent(eventId);
        }

        private List<ushort> mPrivateEventIds = null;

        private List<ushort> mEventIds
        {
            get
            {
                if (null == mPrivateEventIds)
                {
                    mPrivateEventIds = new List<ushort>();
                }

                return mPrivateEventIds;
            }
        }

        protected virtual void OnDestroy()
        {
            OnBeforeDestroy();
            mCurMgr = null;

            if (Application.isPlaying) 
            {
                UnRegisterAllEvent();
            }
        }

        protected virtual void OnBeforeDestroy(){}
    }
}

QMgrBehaivour 維護了一個 QEventSytem 成員。用來在模塊內進行消息的中轉的。
那么如何實現跨模塊之間的消息發送呢?
這里看下 QMsgCenter.cs 就理解了
[C#] 純文本查看 復制代碼
/* Copyright (c) 2017 [email]xiaojun@putao.com[/email]
 * Copyright (c) 2017 liangxie */

namespace QFramework
{
    using UnityEngine;

    [QMonoSingletonPath("[Event]/QMsgCenter")]
    public partial class QMsgCenter : MonoBehaviour, ISingleton
    {
        public static QMsgCenter Instance
        {
            get { return MonoSingletonProperty<QMsgCenter>.Instance; }
        }

        public void OnSingletonInit()
        {

        }

        public void Dispose()
        {
            MonoSingletonProperty<QMsgCenter>.Dispose();
        }

        void Awake()
        {
            DontDestroyOnLoad(this);
        }

        public void SendMsg(QMsg tmpMsg)
        {
            // Framework Msg
            switch (tmpMsg.ManagerID)
            {
                case QMgrID.UI:
                    QUIManager.Instance.SendMsg(tmpMsg);
                    return;
                case QMgrID.Audio:
                    AudioManager.Instance.SendMsg(tmpMsg);
                    return;
            }

            // ForwardMsg(tmpMsg);
        }
    }
}

主要是 SendMsg 這個方法。
每個消息都提供了一個  ManagerID。
什么時候消息會傳遞到  SendMsg 呢?
就是當消息的 Id 不在自己模塊的頻段時候,核心代碼如下:
QMgrBehaivour.cs
[C#] 純文本查看 復制代碼
        public override void SendMsg(QMsg msg)
        {
            if (msg.ManagerID == mMgrId)
            {
                Process(msg.EventID, msg);
            }
            else 
            {
                QMsgCenter.Instance.SendMsg (msg);
            }
        }

不難理解。
跨模塊之間的消息發送順序為:
UIPanel -> UIMgr -> MsgCenter -> CharacterManager -> Character。
關于模塊化消息的介紹就到這里。
小結  (三)
UI Kit 有 Manager Of Managers 框架強力支持。
而 Manager Of Managers 不僅提供了模塊實現的工具 QMgrBehaivour 和 QMonoBehaviour,
還提供了緊實的模塊化消息支持。
而 Manager Of Manager 貫穿整個 QFramework。
從 框架層的 UIMgr、AudioManager、NetworkManager 到 GamePlay 層的 CharacterManager/GameManager (需自己實現) 等,都可以使用。
本 Chat 的兩個重點,一個是 UI 的管理,和一個是事件系統。
  • UI 管理
  • 層級管理
  • 界面生命周期管理
  • 事件系統
  • 全局消息機制
  • 基于模塊的消息機制
除了以上,其實還有一個系統,就是組件系統,整個組件系統的前提是強大的代碼生成支持。
  • UI 組件系統
  • UIMark 標記
    • UIDefaultComponent:   默認的 Button、Image 等 Unity UGUI 提供的空間,自動識別。
    • UIElement: 不可復用的自定義控件。
    • UIComponent: 可復用的自定義控件。

以上整個系統呢在后續的 GitChat 里介紹。
搭建自己的 UI 框架
之前都是手把手的教大家如何去搭建,不如給大家一個執行清單,大家跟著一下的步驟試著自己實現一下。實現完了之后歡迎跟筆者進行更多的探討。
MyUIKit v0.0.1 開始
  • UIRoot Prefab 結構
  • UIRoot(Canvas)
    • EventSystem
    • UICamera
  • UIManager
  • 實現容器
  • 提供動態加載方式
    • Resources
  • 提供 API
    • 加載 UIRoot
    • Open
    • Close
    • Show
    • Hide
  • UIPanel
  • 提供生命周期支持
    • OnOpen
    • OnEnter
    • OnExit
    • OnShow
    • OnHide

MyUIKit v0.0.2 層級管理
  • UIRoot
  • 添加常用層級
  • UIManager
  • 增加層級管理
  • 提供 Open API 關于層級管理的重載支持

MyUIKit v0.0.3 UIData
  • UIData 基類定義
  • UIPanel
  • 提供 UIData 接收參數
  • UIManager
  • Open API 提供 UIData 傳入參數

MyUIKit v0.0.4 堆棧支持
  • UIPanelInfo 支持
  • 記錄層級信息
  • 記錄PanelName
  • 記錄 UIData
  • UIManager
  • 提供 Stack 容器
  • 提供 Push/Back API

MyUIKit v0.0.5 事件機制實現MyUIKit v0.0.6 事件機制集成到基類
  • UIPanel 集成消息注冊列表,在 OnDestory 或者關閉時自動進行卸載操作。
MyUIKit v0.1.1 簡易 UI Kit
到這里已經實現了簡易的 UI Kit,包含了 UI 層級管理、堆棧、界面管理、事件系統等核心功能。
在執行以上清單的過程中遇到問題歡迎隨時與我交流。
如果完成,想知道接下來的開發方向,也請與我交流。
轉載請注明地址:涼鞋的筆記:liangxiegame.com
更多內容
  • QFramework 地址:https://github.com/liangxiegame/QFramework
  • QQ 交流群:623597263
  • Unity 進階小班
    • 主要訓練內容:
      • 框架搭建訓練(第一年)
      • 跟著案例學 Shader(第一年)
      • 副業的孵化(第二年、第三年)
    • 權益、授課形式等具體詳情請查看《小班產品手冊》:https://liangxiegame.com/master/intro
  • 關注公眾號:liangxiegame 獲取第一時間更新通知及更多的免費內容。

回復

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 注冊帳號

本版積分規則

5.1勞動節課程大促銷!27日零點開始!
神马电影34pp影视午夜