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

damenhanter發表於2016-02-02

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

最終的實現效果請拉到最下方檢視

框架具體實現的功能和需求

  • 載入,顯示,隱藏,關閉頁面,根據標示獲得相應介面例項
  • 提供介面顯示隱藏動畫介面
  • 單獨介面層級,Collider,背景管理
  • 根據儲存的導航資訊完成介面導航
  • 介面通用對話方塊管理(多型別Message Box)
  • 便於進行需求和功能擴充套件(比如,在跳出頁面之前新增邏輯處理等)

編寫UI框架意義

  • 開啟,關閉,層級,頁面跳轉等管理問題集中化,將外部切換等邏輯交給UIManager處理
  • 功能邏輯分散化,每個頁面維護自身邏輯,依託於框架便於多人協同開發,不用關心跳轉和顯示關閉細節
  • 通用性框架能夠做到簡單的程式碼複用和"專案經驗"沉澱

步入正題,如何實現

  1. 視窗類設計:基本視窗物件,維護自身邏輯維護
  2. 視窗管理類:控制被管理視窗的開啟和關閉等邏輯(具體設計請看下文)
  3. 動畫介面:提供開啟和關閉動畫介面,提供動畫完成回撥函式等
  4. 層級,Collider背景管理
視窗基類設計
框架中設計的視窗型別和框架所需定義如下

[csharp] view plain copy
  1. public enum UIWindowType  
  2. {  
  3.     Normal,    // 可推出介面(UIMainMenu,UIRank等)  
  4.     Fixed,     // 固定視窗(UITopBar等)  
  5.     PopUp,     // 模式視窗  
  6. }  
  7.   
  8. public enum UIWindowShowMode  
  9. {  
  10.     DoNothing,  
  11.     HideOther,     // 閉其他介面  
  12.     NeedBack,      // 點選返回按鈕關閉當前,不關閉其他介面(需要調整好層級關係)  
  13.     NoNeedBack,    // 關閉TopBar,關閉其他介面,不加入backSequence佇列  
  14. }  
  15.   
  16. public enum UIWindowColliderMode  
  17. {  
  18.     None,      // 顯示該介面不包含碰撞背景  
  19.     Normal,    // 碰撞透明背景  
  20.     WithBg,    // 碰撞非透明背景  
  21. }  


[csharp] view plain copy
  1. using UnityEngine;  
  2. using System.Collections;  
  3. using System;  
  4.   
  5. namespace CoolGame  
  6. {  
  7.     /// <summary>  
  8.     /// 視窗基類  
  9.     /// </summary>  
  10.     public class UIBaseWindow : MonoBehaviour  
  11.     {  
  12.         protected UIPanel originPanel;  
  13.   
  14.         // 如果需要可以新增一個BoxCollider遮蔽事件  
  15.         private bool isLock = false;  
  16.         protected bool isShown = false;  
  17.   
  18.         // 當前介面ID  
  19.         protected WindowID windowID = WindowID.WindowID_Invaild;  
  20.   
  21.         // 指向上一級介面ID(BackSequence無內容,返回上一級)  
  22.         protected WindowID preWindowID = WindowID.WindowID_Invaild;  
  23.         public WindowData windowData = new WindowData();  
  24.   
  25.         // Return處理邏輯  
  26.         private event BoolDelegate returnPreLogic = null;  
  27.   
  28.         protected Transform mTrs;  
  29.         protected virtual void Awake()  
  30.         {  
  31.             this.gameObject.SetActive(true);  
  32.             mTrs = this.gameObject.transform;  
  33.             InitWindowOnAwake();  
  34.         }  
  35.   
  36.         private int minDepth = 1;  
  37.         public int MinDepth  
  38.         {  
  39.             get { return minDepth; }  
  40.             set { minDepth = value; }  
  41.         }  
  42.   
  43.         /// <summary>  
  44.         /// 能否新增到導航資料中  
  45.         /// </summary>  
  46.         public bool CanAddedToBackSeq  
  47.         {  
  48.             get  
  49.             {  
  50.                 if (this.windowData.windowType == UIWindowType.PopUp)  
  51.                     return false;  
  52.                 if (this.windowData.windowType == UIWindowType.Fixed)  
  53.                     return false;  
  54.                 if (this.windowData.showMode == UIWindowShowMode.NoNeedBack)  
  55.                     return false;  
  56.                 return true;  
  57.             }  
  58.         }  
  59.   
  60.         /// <summary>  
  61.         /// 介面是否要重新整理BackSequence資料  
  62.         /// 1.顯示NoNeedBack或者從NoNeedBack顯示新介面 不更新BackSequenceData(隱藏自身即可)  
  63.         /// 2.HideOther  
  64.         /// 3.NeedBack  
  65.         /// </summary>  
  66.         public bool RefreshBackSeqData  
  67.         {  
  68.             get  
  69.             {  
  70.                 if (this.windowData.showMode == UIWindowShowMode.HideOther  
  71.                     || this.windowData.showMode == UIWindowShowMode.NeedBack)  
  72.                     return true;  
  73.                 return false;  
  74.             }  
  75.         }  
  76.   
  77.         /// <summary>  
  78.         /// 在Awake中呼叫,初始化介面(給介面元素賦值操作)  
  79.         /// </summary>  
  80.         public virtual void InitWindowOnAwake()  
  81.         {  
  82.         }  
  83.   
  84.         /// <summary>  
  85.         /// 獲得該視窗管理類  
  86.         /// </summary>  
  87.         public UIManagerBase GetWindowManager  
  88.         {  
  89.             get  
  90.             {  
  91.                 UIManagerBase baseManager = this.gameObject.GetComponent<UIManagerBase>();  
  92.                 return baseManager;  
  93.             }  
  94.             private set { }  
  95.         }  
  96.   
  97.         /// <summary>  
  98.         /// 重置視窗  
  99.         /// </summary>  
  100.         public virtual void ResetWindow()  
  101.         {  
  102.         }  
  103.   
  104.         /// <summary>  
  105.         /// 初始化視窗資料  
  106.         /// </summary>  
  107.         public virtual void InitWindowData()  
  108.         {  
  109.             if (windowData == null)  
  110.                 windowData = new WindowData();  
  111.         }  
  112.   
  113.         public virtual void ShowWindow()  
  114.         {  
  115.             isShown = true;  
  116.             NGUITools.SetActive(this.gameObject, true);  
  117.         }  
  118.   
  119.         public virtual void HideWindow(Action action = null)  
  120.         {  
  121.             IsLock = true;  
  122.             isShown = false;  
  123.             NGUITools.SetActive(this.gameObject, false);  
  124.             if (action != null)  
  125.                 action();  
  126.         }  
  127.   
  128.         public void HideWindowDirectly()  
  129.         {  
  130.             IsLock = true;  
  131.             isShown = false;  
  132.             NGUITools.SetActive(this.gameObject, false);  
  133.         }  
  134.   
  135.         public virtual void DestroyWindow()  
  136.         {  
  137.             BeforeDestroyWindow();  
  138.             GameObject.Destroy(this.gameObject);  
  139.         }  
  140.   
  141.         protected virtual void BeforeDestroyWindow()  
  142.         {  
  143.         }  
  144.   
  145.         /// <summary>  
  146.         /// 介面在退出或者使用者點選返回之前都可以註冊執行邏輯  
  147.         /// </summary>  
  148.         protected void RegisterReturnLogic(BoolDelegate newLogic)  
  149.         {  
  150.             returnPreLogic = newLogic;  
  151.         }  
  152.   
  153.         public bool ExecuteReturnLogic()  
  154.         {  
  155.             if (returnPreLogic == null)  
  156.                 return false;  
  157.             else  
  158.                 return returnPreLogic();  
  159.         }  
  160.     }  
  161. }  
動畫介面設計
介面可以繼承該介面進行實現開啟和關閉動畫
[csharp] view plain copy
  1. /// <summary>  
  2. /// 視窗動畫  
  3. /// </summary>  
  4. interface IWindowAnimation  
  5. {  
  6.     /// <summary>  
  7.     /// 顯示動畫  
  8.     /// </summary>  
  9.     void EnterAnimation(EventDelegate.Callback onComplete);  
  10.       
  11.     /// <summary>  
  12.     /// 隱藏動畫  
  13.     /// </summary>  
  14.     void QuitAnimation(EventDelegate.Callback onComplete);  
  15.       
  16.     /// <summary>  
  17.     /// 重置動畫  
  18.     /// </summary>  
  19.     void ResetAnimation();  
  20. }  
[csharp] view plain copy
  1. public void EnterAnimation(EventDelegate.Callback onComplete)  
  2. {  
  3.     if (twAlpha != null)  
  4.     {  
  5.         twAlpha.PlayForward();  
  6.         EventDelegate.Set(twAlpha.onFinished, onComplete);  
  7.     }  
  8. }  
  9.   
  10. public void QuitAnimation(EventDelegate.Callback onComplete)  
  11. {  
  12.     if (twAlpha != null)  
  13.     {  
  14.         twAlpha.PlayReverse();  
  15.         EventDelegate.Set(twAlpha.onFinished, onComplete);  
  16.     }  
  17. }  
  18.   
  19. public override void ResetWindow()  
  20. {  
  21.     base.ResetWindow();  
  22.     ResetAnimation();  
  23. }  


視窗管理和導航設計實現
導航功能實現通過一個顯示視窗堆疊實現,每次開啟和關閉視窗通過判斷視窗屬性和型別更新處理BackSequence資料
  • 開啟介面:將當前介面狀態壓入堆疊中更新BackSequence資料
  • 返回操作(主動關閉當前介面或者點選返回按鈕):從堆疊中Pop出一個介面狀態,將相應的介面重新開啟
  • 怎麼銜接:比如從一個介面沒有回到上一個狀態而是直接的跳轉到其他的介面,這個時候需要將BackSequence清空因為當前的導航鏈已經被破壞,當BackSequence為空需要根據當前視窗指定的PreWindowId告知系統當從該介面返回,需要到達的指定頁面,這樣就能解決怎麼銜接的問題,如果沒斷,繼續執行導航,否則清空資料,根據PreWindowId進行導航
導航系統中關鍵性設計:
遊戲中可以存在多個的Manager進行管理(一般在很少需求下才會使用),每個管理物件需要維護自己的導航資訊BackSequence,每次退出一個介面需要檢測當前退出的介面是否存在相應的Manager管理,如果存在則需要先執行Manager退出操作(退出過程分步進行)保證介面一層接著一層正確退出

視窗層級,Collider,統一背景新增如何實現?
有很多方式進行層級管理,該框架選擇的方法如下
  • 設定三個常用層級Root,根據視窗型別在載入到遊戲中時新增到對應的層級Root下面即可,每次新增重新計算設定層級(通過UIPanel的depth實現)保證每次開啟一個新視窗層級顯示正確,每次視窗內通過depth的大小區分層級關係
  • 根據視窗Collider和背景型別,在視窗的最小Panel上面新增Collider或者帶有碰撞體的BackGround即可

具體實現如下:
[csharp] view plain copy
  1. private void AdjustBaseWindowDepth(UIBaseWindow baseWindow)  
  2. {  
  3.     UIWindowType windowType = baseWindow.windowData.windowType;  
  4.     int needDepth = 1;  
  5.     if (windowType == UIWindowType.Normal)  
  6.     {  
  7.         needDepth = Mathf.Clamp(GameUtility.GetMaxTargetDepth(UINormalWindowRoot.gameObject, false) + 1, normalWindowDepth, int.MaxValue);  
  8.         Debug.Log("[UIWindowType.Normal] maxDepth is " + needDepth + baseWindow.GetID);  
  9.     }  
  10.     else if (windowType == UIWindowType.PopUp)  
  11.     {  
  12.         needDepth = Mathf.Clamp(GameUtility.GetMaxTargetDepth(UIPopUpWindowRoot.gameObject) + 1, popUpWindowDepth, int.MaxValue);  
  13.         Debug.Log("[UIWindowType.PopUp] maxDepth is " + needDepth);  
  14.     }  
  15.     else if (windowType == UIWindowType.Fixed)  
  16.     {  
  17.         needDepth = Mathf.Clamp(GameUtility.GetMaxTargetDepth(UIFixedWidowRoot.gameObject) + 1, fixedWindowDepth, int.MaxValue);  
  18.         Debug.Log("[UIWindowType.Fixed] max depth is " + needDepth);  
  19.     }  
  20.     if(baseWindow.MinDepth != needDepth)  
  21.         GameUtility.SetTargetMinPanel(baseWindow.gameObject, needDepth);  
  22.     baseWindow.MinDepth = needDepth;  
  23. }  
  24.   
  25. /// <summary>  
  26. /// 視窗背景碰撞體處理  
  27. /// </summary>  
  28. private void AddColliderBgForWindow(UIBaseWindow baseWindow)  
  29. {  
  30.     UIWindowColliderMode colliderMode = baseWindow.windowData.colliderMode;  
  31.     if (colliderMode == UIWindowColliderMode.None)  
  32.         return;  
  33.   
  34.     if (colliderMode == UIWindowColliderMode.Normal)  
  35.         GameUtility.AddColliderBgToTarget(baseWindow.gameObject, "Mask02", maskAtlas, true);  
  36.     if (colliderMode == UIWindowColliderMode.WithBg)  
  37.         GameUtility.AddColliderBgToTarget(baseWindow.gameObject, "Mask02", maskAtlas, false);  
  38. }  

多形態MessageBox實現
這個應該是專案中一定會用到的功能,說下該框架簡單的實現
  • 三個按鈕三種回撥邏輯:左中右三個按鈕,提供設定內容,設定回撥函式的介面即可
  • 提供介面設定核心Content
  • 不同作用下不同的按鈕不會隱藏和顯示
[csharp] view plain copy
  1. public void SetCenterBtnCallBack(string msg, UIEventListener.VoidDelegate callBack)  
  2. {  
  3.     lbCenter.text = msg;  
  4.     NGUITools.SetActive(btnCenter, true);  
  5.     UIEventListener.Get(btnCenter).onClick = callBack;  
  6. }  
  7.   
  8. public void SetLeftBtnCallBack(string msg, UIEventListener.VoidDelegate callBack)  
  9. {  
  10.     lbLeft.text = msg;  
  11.     NGUITools.SetActive(btnLeft, true);  
  12.     UIEventListener.Get(btnLeft).onClick = callBack;  
  13. }  
  14.   
  15. public void SetRightBtnCallBack(string msg, UIEventListener.VoidDelegate callBack)  
  16. {  
  17.     lbRight.text = msg;  
  18.     NGUITools.SetActive(btnRight, true);  
  19.     UIEventListener.Get(btnRight).onClick = callBack;  
  20. }  

後續需要改進和增強計劃

  1. 圖集管理,針對大中型遊戲對遊戲記憶體要求苛刻的專案,一般都會對UI圖集貼圖資源進行動態管理,載入和解除安裝圖集,保證UI貼圖佔用較少記憶體
  2. 增加一些通用處理:變灰操作,Mask遮罩(一般用於新手教程中)等
  3. 在進行切換的過程可以需要Load新場景需求,雖然這個也可以在UI框架外實現
  4. 對話系統也算是UI框架的功能,新手引導系統也可以加入到UI框架中,統一管理和處理新手引導邏輯
需求總是驅動著系統逐漸強大,逐漸完善,逐漸發展,一步一步來吧~

實現效果



整個框架的核心部分介紹完畢,有需要檢視原始碼的請移步GitHub,後續會繼續完善和整理,希望能夠給耐心看到結尾的朋友一點啟發或者帶來一點幫助,存在錯誤和改進的地方也希望留言交流共同進步學習~


有些時候,我們總是知道這麼個理明白該怎樣實現,但是關鍵的就是要動手實現出來,實現的過程會發現自己的想法在慢慢優化,不斷的需求和bug的產生讓框架慢慢成熟,可以投入專案使用提升一些開發效率和減少工作量。


By 漂流燕(Andy)

相關文章