【设计和开发一套简单自动化UI框架】

目标:编写一个简单通用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.     ///   
  8.     /// 窗口基类  
  9.     ///   
  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.         ///   
  44.         /// 能否添加到导航数据中  
  45.         ///   
  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.         ///   
  61.         /// 界面是否要刷新BackSequence数据  
  62.         /// 1.显示NoNeedBack或者从NoNeedBack显示新界面 不更新BackSequenceData(隐藏自身即可)  
  63.         /// 2.HideOther  
  64.         /// 3.NeedBack  
  65.         ///   
  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.         ///   
  78.         /// 在Awake中调用,初始化界面(给界面元素赋值操作)  
  79.         ///   
  80.         public virtual void InitWindowOnAwake()  
  81.         {  
  82.         }  
  83.   
  84.         ///   
  85.         /// 获得该窗口管理类  
  86.         ///   
  87.         public UIManagerBase GetWindowManager  
  88.         {  
  89.             get  
  90.             {  
  91.                 UIManagerBase baseManager = this.gameObject.GetComponent();  
  92.                 return baseManager;  
  93.             }  
  94.             private set { }  
  95.         }  
  96.   
  97.         ///   
  98.         /// 重置窗口  
  99.         ///   
  100.         public virtual void ResetWindow()  
  101.         {  
  102.         }  
  103.   
  104.         ///   
  105.         /// 初始化窗口数据  
  106.         ///   
  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.         ///   
  146.         /// 界面在退出或者用户点击返回之前都可以注册执行逻辑  
  147.         ///   
  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. ///   
  2. /// 窗口动画  
  3. ///   
  4. interface IWindowAnimation  
  5. {  
  6.     ///   
  7.     /// 显示动画  
  8.     ///   
  9.     void EnterAnimation(EventDelegate.Callback onComplete);  
  10.       
  11.     ///   
  12.     /// 隐藏动画  
  13.     ///   
  14.     void QuitAnimation(EventDelegate.Callback onComplete);  
  15.       
  16.     ///   
  17.     /// 重置动画  
  18.     ///   
  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即可
【设计和开发一套简单自动化UI框架】_第1张图片【设计和开发一套简单自动化UI框架】_第2张图片
具体实现如下:
[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. ///   
  26. /// 窗口背景碰撞体处理  
  27. ///   
  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)

你可能感兴趣的:(U3D)