1. 程式人生 > >【設計和開發一套簡單自己主動化UI框架】

【設計和開發一套簡單自己主動化UI框架】

depth 定義 其它 而是 例如 選擇 span debug etl

!有興趣的朋友請直接移步Github,本帖子已經不做更新,框架的詳細的實現已經做了優化和代碼整理,本文僅僅介紹了詳細的設計思路!


目標:編寫一個簡單通用UI框架用於管理頁面和完畢導航跳轉

終於的實現效果請拉到最下方查看

框架詳細實現的功能和需求

  • 載入。顯示,隱藏,關閉頁面,依據標示獲得對應界面實例
  • 提供界面顯示隱藏動畫接口
  • 單獨界面層級。Collider。背景管理
  • 依據存儲的導航信息完畢界面導航
  • 界面通用對話框管理(多類型Message Box)
  • 便於進行需求和功能擴展(比方,在跳出頁面之前加入邏輯處理等)

編寫UI框架意義

  • 打開,關閉,層級,頁面跳轉等管理問題集中化,將外部切換等邏輯交給UIManager處理
  • 功能邏輯分散化,每一個頁面維護自身邏輯,依托於框架便於多人協同開發,不用關心跳轉和顯示關閉細節
  • 通用性框架可以做到簡單的代碼復用和"項目經驗"沈澱

步入正題,怎樣實現

  1. 窗體類設計:基本窗體對象,維護自身邏輯維護
  2. 窗體管理類:控制被管理窗體的打開和關閉等邏輯(詳細設計請看下文)
  3. 動畫接口:提供打開和關閉動畫接口,提供動畫完畢回調函數等
  4. 層級,Collider背景管理
窗體基類設計 框架中設計的窗體類型和框架所需定義例如以下
    public enum UIWindowType
    {
        Normal,    // 可推出界面(UIMainMenu,UIRank等)
        Fixed,     // 固定窗體(UITopBar等)
        PopUp,     // 模式窗體
    }

    public enum UIWindowShowMode
    {
        DoNothing,
        HideOther,     // 閉其它界面
        NeedBack,      // 點擊返回button關閉當前,不關閉其它界面(須要調整好層級關系)
        NoNeedBack,    // 關閉TopBar,關閉其它界面,不增加backSequence隊列
    }

    public enum UIWindowColliderMode
    {
        None,      // 顯示該界面不包括碰撞背景
        Normal,    // 碰撞透明背景
        WithBg,    // 碰撞非透明背景
    }


using UnityEngine;
using System.Collections;
using System;

namespace CoolGame
{
    /// <summary>
    /// 窗體基類
    /// </summary>
    public class UIBaseWindow : MonoBehaviour
    {
        protected UIPanel originPanel;

        // 假設須要能夠加入一個BoxCollider屏蔽事件
        private bool isLock = false;
        protected bool isShown = false;

        // 當前界面ID
        protected WindowID windowID = WindowID.WindowID_Invaild;

        // 指向上一級界面ID(BackSequence無內容,返回上一級)
        protected WindowID preWindowID = WindowID.WindowID_Invaild;
        public WindowData windowData = new WindowData();

        // Return處理邏輯
        private event BoolDelegate returnPreLogic = null;

        protected Transform mTrs;
        protected virtual void Awake()
        {
            this.gameObject.SetActive(true);
            mTrs = this.gameObject.transform;
            InitWindowOnAwake();
        }

        private int minDepth = 1;
        public int MinDepth
        {
            get { return minDepth; }
            set { minDepth = value; }
        }

        /// <summary>
        /// 是否能加入到導航數據中
        /// </summary>
        public bool CanAddedToBackSeq
        {
            get
            {
                if (this.windowData.windowType == UIWindowType.PopUp)
                    return false;
                if (this.windowData.windowType == UIWindowType.Fixed)
                    return false;
                if (this.windowData.showMode == UIWindowShowMode.NoNeedBack)
                    return false;
                return true;
            }
        }

        /// <summary>
        /// 界面是否要刷新BackSequence數據
        /// 1.顯示NoNeedBack或者從NoNeedBack顯示新界面 不更新BackSequenceData(隱藏自身就可以)
        /// 2.HideOther
        /// 3.NeedBack
        /// </summary>
        public bool RefreshBackSeqData
        {
            get
            {
                if (this.windowData.showMode == UIWindowShowMode.HideOther
                    || this.windowData.showMode == UIWindowShowMode.NeedBack)
                    return true;
                return false;
            }
        }

        /// <summary>
        /// 在Awake中調用。初始化界面(給界面元素賦值操作)
        /// </summary>
        public virtual void InitWindowOnAwake()
        {
        }

        /// <summary>
        /// 獲得該窗體管理類
        /// </summary>
        public UIManagerBase GetWindowManager
        {
            get
            {
                UIManagerBase baseManager = this.gameObject.GetComponent<UIManagerBase>();
                return baseManager;
            }
            private set { }
        }

        /// <summary>
        /// 重置窗體
        /// </summary>
        public virtual void ResetWindow()
        {
        }

        /// <summary>
        /// 初始化窗體數據
        /// </summary>
        public virtual void InitWindowData()
        {
            if (windowData == null)
                windowData = new WindowData();
        }

        public virtual void ShowWindow()
        {
            isShown = true;
            NGUITools.SetActive(this.gameObject, true);
        }

        public virtual void HideWindow(Action action = null)
        {
            IsLock = true;
            isShown = false;
            NGUITools.SetActive(this.gameObject, false);
            if (action != null)
                action();
        }

        public void HideWindowDirectly()
        {
            IsLock = true;
            isShown = false;
            NGUITools.SetActive(this.gameObject, false);
        }

        public virtual void DestroyWindow()
        {
            BeforeDestroyWindow();
            GameObject.Destroy(this.gameObject);
        }

        protected virtual void BeforeDestroyWindow()
        {
        }

        /// <summary>
        /// 界面在退出或者用戶點擊返回之前都能夠註冊運行邏輯
        /// </summary>
        protected void RegisterReturnLogic(BoolDelegate newLogic)
        {
            returnPreLogic = newLogic;
        }

        public bool ExecuteReturnLogic()
        {
            if (returnPreLogic == null)
                return false;
            else
                return returnPreLogic();
        }
    }
}
動畫接口設計 界面能夠繼承該接口進行實現打開和關閉動畫
    /// <summary>
    /// 窗體動畫
    /// </summary>
    interface IWindowAnimation
    {
        /// <summary>
        /// 顯示動畫
        /// </summary>
        void EnterAnimation(EventDelegate.Callback onComplete);
        
        /// <summary>
        /// 隱藏動畫
        /// </summary>
        void QuitAnimation(EventDelegate.Callback onComplete);
        
        /// <summary>
        /// 重置動畫
        /// </summary>
        void ResetAnimation();
    }
        public void EnterAnimation(EventDelegate.Callback onComplete)
        {
            if (twAlpha != null)
            {
                twAlpha.PlayForward();
                EventDelegate.Set(twAlpha.onFinished, onComplete);
            }
        }

        public void QuitAnimation(EventDelegate.Callback onComplete)
        {
            if (twAlpha != null)
            {
                twAlpha.PlayReverse();
                EventDelegate.Set(twAlpha.onFinished, onComplete);
            }
        }

        public override void ResetWindow()
        {
            base.ResetWindow();
            ResetAnimation();
        }


窗體管理和導航設計實現 導航功能實現通過一個顯示窗體堆棧實現。每次打開和關閉窗體通過推斷窗體屬性和類型更新處理BackSequence數據
  • 打開界面:將當前界面狀態壓入堆棧中更新BackSequence數據
  • 返回操作(主動關閉當前界面或者點擊返回button):從堆棧中Pop出一個界面狀態,將對應的界面又一次打開
  • 怎麽銜接:比方從一個界面沒有回到上一個狀態而是直接的跳轉到其它的界面,這個時候須要將BackSequence清空由於當前的導航鏈已經被破壞。當BackSequence為空須要依據當前窗體指定的PreWindowId告知系統當從該界面返回,須要到達的指定頁面。這樣就能解決怎麽銜接的問題,假設沒斷,繼續運行導航,否則清空數據,依據PreWindowId進行導航
導航系統中關鍵性設計: 遊戲中能夠存在多個的Manager進行管理(一般在非常少需求下才會使用),每一個管理對象須要維護自己的導航信息BackSequence。每次退出一個界面須要檢測當前退出的界面是否存在對應的Manager管理,假設存在則須要先運行Manager退出操作(退出過程分步進行)保證界面一層接著一層正確退出
窗體層級,Collider。統一背景加入怎樣實現? 有非常多方式進行層級管理,該框架選擇的方法例如以下
  • 設置三個經常使用層級Root,依據窗體類型在載入到遊戲中時加入到相應的層級Root以下就可以。每次加入又一次計算設置層級(通過UIPanel的depth實現)保證每次打開一個新窗體層級顯示正確,每次窗體內通過depth的大小區分層級關系
  • 依據窗體Collider和背景類型,在窗體的最小Panel上面加入Collider或者帶有碰撞體的BackGround就可以
技術分享技術分享
詳細實現例如以下:
private void AdjustBaseWindowDepth(UIBaseWindow baseWindow)
{
    UIWindowType windowType = baseWindow.windowData.windowType;
    int needDepth = 1;
    if (windowType == UIWindowType.Normal)
    {
        needDepth = Mathf.Clamp(GameUtility.GetMaxTargetDepth(UINormalWindowRoot.gameObject, false) + 1, normalWindowDepth, int.MaxValue);
        Debug.Log("[UIWindowType.Normal] maxDepth is " + needDepth + baseWindow.GetID);
    }
    else if (windowType == UIWindowType.PopUp)
    {
        needDepth = Mathf.Clamp(GameUtility.GetMaxTargetDepth(UIPopUpWindowRoot.gameObject) + 1, popUpWindowDepth, int.MaxValue);
        Debug.Log("[UIWindowType.PopUp] maxDepth is " + needDepth);
    }
    else if (windowType == UIWindowType.Fixed)
    {
        needDepth = Mathf.Clamp(GameUtility.GetMaxTargetDepth(UIFixedWidowRoot.gameObject) + 1, fixedWindowDepth, int.MaxValue);
        Debug.Log("[UIWindowType.Fixed] max depth is " + needDepth);
    }
    if(baseWindow.MinDepth != needDepth)
        GameUtility.SetTargetMinPanel(baseWindow.gameObject, needDepth);
    baseWindow.MinDepth = needDepth;
}

/// <summary>
/// 窗體背景碰撞體處理
/// </summary>
private void AddColliderBgForWindow(UIBaseWindow baseWindow)
{
    UIWindowColliderMode colliderMode = baseWindow.windowData.colliderMode;
    if (colliderMode == UIWindowColliderMode.None)
        return;

    if (colliderMode == UIWindowColliderMode.Normal)
        GameUtility.AddColliderBgToTarget(baseWindow.gameObject, "Mask02", maskAtlas, true);
    if (colliderMode == UIWindowColliderMode.WithBg)
        GameUtility.AddColliderBgToTarget(baseWindow.gameObject, "Mask02", maskAtlas, false);
}

多形態MessageBox實現
這個應該是項目中一定會用到的功能,說下該框架簡單的實現
  • 三個button三種回調邏輯:左中右三個button,提供設置內容,設置回調函數的接口就可以
  • 提供接口設置核心Content
  • 不同作用下不同的button不會隱藏和顯示
public void SetCenterBtnCallBack(string msg, UIEventListener.VoidDelegate callBack)
{
    lbCenter.text = msg;
    NGUITools.SetActive(btnCenter, true);
    UIEventListener.Get(btnCenter).onClick = callBack;
}

public void SetLeftBtnCallBack(string msg, UIEventListener.VoidDelegate callBack)
{
    lbLeft.text = msg;
    NGUITools.SetActive(btnLeft, true);
    UIEventListener.Get(btnLeft).onClick = callBack;
}

public void SetRightBtnCallBack(string msg, UIEventListener.VoidDelegate callBack)
{
    lbRight.text = msg;
    NGUITools.SetActive(btnRight, true);
    UIEventListener.Get(btnRight).onClick = callBack;
}

興許須要改進和增強計劃

  1. 圖集管理,針對大中型遊戲對遊戲內存要求苛刻的項目,一般都會對UI圖集貼圖資源進行動態管理,載入和卸載圖集,保證UI貼圖占用較少內存
  2. 添加一些通用處理:變灰操作,Mask遮罩(一般用於入門教程中)等
  3. 在進行切換的過程能夠須要Load新場景需求,盡管這個也能夠在UI框架外實現
  4. 對話系統也算是UI框架的功能,新手引導系統也能夠添加到UI框架中,統一管理和處理新手引導邏輯
需求總是驅動著系統逐漸強大,逐漸完好,逐漸發展,一步一步來吧~

實現效果

技術分享


整個框架的核心部分介紹完成,有須要查看源代碼的請移步GitHub。興許會繼續完好和整理,希望可以給耐心看到結尾的朋友一點啟示或者帶來一點幫助。存在錯誤和改進的地方也希望留言交流共同進步學習~


有些時候,我們總是知道這麽個理明確該如何實現。可是關鍵的就是要動手實現出來,實現的過程會發現自己的想法在慢慢優化。不斷的需求和bug的產生讓框架慢慢成熟,能夠投入項目使用提升一些開發效率和降低工作量。

【設計和開發一套簡單自己主動化UI框架】