Unity3D通用UI框架

 



目标:编写一个简单通用UI框架用于管理页面和完成导航跳转最终的实现效果请拉到最下方查看

框架具体实现的功能和需求
  • 加载,显示,隐藏,关闭页面,根据标示获得相应界面实例
  • 提供界面显示隐藏动画接口
  • 单独界面层级,Collider,背景管理
  • 根据存储的导航信息完成界面导航
  • 界面通用对话框管理(多类型Message Box)
  • 便于进行需求和功能扩展(比如,在跳出页面之前添加逻辑处理等)
编写UI框架意义
  • 打开,关闭,层级,页面跳转等管理问题集中化,将外部切换等逻辑交给UIManager处理
  • 功能逻辑分散化,每个页面维护自身逻辑,依托于框架便于多人协同开发,不用关心跳转和显示关闭细节
  • 通用性框架能够做到简单的代码复用和"项目经验"沉淀
步入正题,如何实现
  • 窗口类设计:基本窗口对象,维护自身逻辑维护
  • 窗口管理类:控制被管理窗口的打开和关闭等逻辑(具体设计请看下文)
  • 动画接口:提供打开和关闭动画接口,提供动画完成回调函数等
  • 层级,Collider背景管理
窗口基类设计



框架中设计的窗口类型和框架所需定义如下
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public enum UIWindowType
{
    Normal,    // 可推出界面(UIMainMenu,UIRank等)
    Fixed,     // 固定窗口(UITopBar等)
    PopUp,     // 模式窗口
}
 
public enum UIWindowShowMode
{
    DoNothing,
    HideOther,     // 闭其他界面
    NeedBack,      // 点击返回按钮关闭当前,不关闭其他界面(需要调整好层级关系)
    NoNeedBack,    // 关闭TopBar,关闭其他界面,不加入backSequence队列
}
 
public enum UIWindowColliderMode
{
    None,      // 显示该界面不包含碰撞背景
    Normal,    // 碰撞透明背景
    WithBg,    // 碰撞非透明背景
}


[C#] 纯文本查看 复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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();
        }
    }
}


动画接口设计
界面可以继承该接口进行实现打开和关闭动画
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// 窗口动画
/// </summary>
interface IWindowAnimation
{
    /// <summary>
    /// 显示动画
    /// </summary>
    void EnterAnimation(EventDelegate.Callback onComplete);
     
    /// <summary>
    /// 隐藏动画
    /// </summary>
    void QuitAnimation(EventDelegate.Callback onComplete);
     
    /// <summary>
    /// 重置动画
    /// </summary>
    void ResetAnimation();
}

[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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数据
  • 返回操作(主动关闭当前界面或者点击返回按钮):从堆栈中Pop出一个界面状态,将相应的界面重新打开
  • 怎么衔接:比如从一个界面没有回到上一个状态而是直接的跳转到其他的界面,这个时候需要将BackSequence清空因为当前的导航链已经被破坏,当BackSequence为空需要根据当前窗口指定的PreWindowId告知系统当从该界面返回,需要到达的指定页面,这样就能解决怎么衔接的问题,如果没断,继续执行导航,否则清空数据,根据PreWindowId进行导航

导航系统中关键性设计:
游戏中可以存在多个的Manager进行管理(一般在很少需求下才会使用),每个管理对象需要维护自己的导航信息BackSequence,每次退出一个界面需要检测当前退出的界面是否存在相应的Manager管理,如果存在则需要先执行Manager退出操作(退出过程分步进行)保证界面一层接着一层正确退出


窗口层级,Collider,统一背景添加如何实现?
有很多方式进行层级管理,该框架选择的方法如下
  • 设置三个常用层级Root,根据窗口类型在加载到游戏中时添加到对应的层级Root下面即可,每次添加重新计算设置层级(通过UIPanel的depth实现)保证每次打开一个新窗口层级显示正确,每次窗口内通过depth的大小区分层级关系
  • 根据窗口Collider和背景类型,在窗口的最小Panel上面添加Collider或者带有碰撞体的BackGround即可
<ignore_js_op>Scene目录<ignore_js_op>层级信息 


具体实现如下:
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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实现
这个应该是项目中一定会用到的功能,说下该框架简单的实现
  • 三个按钮三种回调逻辑:左中右三个按钮,提供设置内容,设置回调函数的接口即可
  • 提供接口设置核心Content
  • 不同作用下不同的按钮不会隐藏和显示
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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;
}




后续需要改进和增强计划
  • 图集管理,针对大中型游戏对游戏内存要求苛刻的项目,一般都会对UI图集贴图资源进行动态管理,加载和卸载图集,保证UI贴图占用较少内存
  • 增加一些通用处理:变灰操作,Mask遮罩(一般用于新手教程中)等
  • 在进行切换的过程可以需要Load新场景需求,虽然这个也可以在UI框架外实现
  • 对话系统也算是UI框架的功能,新手引导系统也可以加入到UI框架中,统一管理和处理新手引导逻辑
需求总是驱动着系统逐渐强大,逐渐完善,逐渐发展,一步一步来吧~

实现效果
<ignore_js_op>效果演示 

整个框架的核心部分介绍完毕,这是项目Demo的下载链接,有需要的朋友感兴趣的朋友可以下载参考下,希望能够给耐心看到结尾的朋友一点启发或者带来一点帮助,存在错误和改进的地方也希望留言交流共同进步学习~


有些时候,我们总是知道这么个理明白该怎样实现,但是关键的就是要动手实现出来,实现的过程会发现自己的想法在慢慢优化,不断的需求和bug的产生让框架慢慢成熟,可以投入项目使用提升一些开发效率和减少工作量。


By 漂流燕(Andy)

posted @ 2015-12-06 23:30  jackcap  阅读(1885)  评论(0编辑  收藏  举报