从宏观角度看 UI 框架
一个项目中拥有众多 UI,每个界面有很多组件;有不少界面是通用的;也有不少界面是父子关系;界面上的按钮需要有一个处理输入的句柄。基于这些思考,可以编写处统一的管理类、基类和输入事件响应机制。
1、管理类
我们需要用一个单实例来管理所有的 UI,让它们有统一的接口进行以上的操作,创建UI管理类是最好的选择,我们可以命名它为 UIManager,这个名字符合它代表的功能。
具体作用:
- 创建 UI
- 查找现有的 UI
- 销毁 UI
- 完成 UI 的统一接口调用和调配工作
存储内容:
- UI 实例
- UI 常用变量,比如屏幕的适配标准大小、Camara 等。
UIManager 是 UI 的管理员,统筹管理 UI 问题。其中涉及的管理包括上下层 UI 切换、不同的加载方式(预加载 UI 、销毁和隐藏)等,其源码如下。
public class ScreenManager : CSingleton<ScreenManager> { protected Transform _transform = null; private Dictionary<string, UIScreenBase> _DicScreens = new Dictionary<string, UIScreenBase>(); // 关闭所有界面 public void CloseAll() { ... } // 是否UI正打开 public bool IsShow(string screenID) { ... } // 关闭界面 public void CloseScreen(UIScreenBase screen) { ... } // 创建所有界面 public T CreateMenu<T>() where T : UIScreenBase { ... } // 找出某个界面 public T FindMenu<T>() where T : UIScreenBase { ... } ... }
2、基类
项目中有很多界面,这些界面都有一定的共性。共性产生统一特征的接口,如Init、Open和Close等。
继承基类可使管理比较方便,比如上面提到的 UIManager 里的 UI 实例可以统一使用基类的方式存储。UIScreenBase
public abstract class UIScreenBase : MonoBehaviour { protected bool mInitialized = false; protected UIState mState = UIState.None; public UIState State { get { return mState; } } public delegate void OnScreenHandlerEventHandler(UIScreenBase screen); public event OnScreenHandlerEventHandler onCloseScreen; // 初始化 protected virtual void Init() { mInitialized = true; } //打开 public virtual void Open() {} //关闭 public virtual void Close() {} }
每个界面都继承自 UI 基类,每个界面成为扩展界面功能的一个类实体,可以自主定义自己的功能性的接口,同时还会受到管理类的统一调配。
3、输入事件响应机制
Unity3D 的 UGUI 输入事件响应机制建立通常有两种,一种是继承型,一种是绑定型。
继承型
事件先响应到基类,再由基类反应给父类,由父类做处理,这样 UI 既可以得到对输入事件的响应,也可以自行修改自己需要的逻辑。
绑定型
在对输入事件响应之前,为 UI 元素绑定一个事件响应的组件。
编写一个绑定型事件类 UIEvent,当某个 UI 元素需要输入事件回调时,对这个物体绑定一个 UIEvent,并且对 UIEvent 里需要的相关响应事件进行赋值或注册操作函数。当输入事件响应时,由 UIEvent 来区分输入的是什么类型的事件,再分别调用响应到具体函数。
共同点:都需要与UI元素关联
区别:继承型融入在了各种组件内,而绑定型以独立的组件形式体现出来的
/// <summary> /// UI 事件 /// </summary> public class UI_Event : UnityEngine.EventSystems.EventTrigger { protected const float CLICK_INTERVAL_TIME = 0.2f; //const click interval time protected const float CLICK_INTERVAL_POS = 2; //const click interval pos public delegate void PointerEventDelegate ( PointerEventData eventData , UI_Event ev); public delegate void BaseEventDelegate ( BaseEventData eventData , UI_Event ev); public delegate void AxisEventDelegate ( AxisEventData eventData , UI_Event ev); public Dictionary<string,object> mArg = new Dictionary<string,object>(); public BaseEventDelegate onDeselect = null; public PointerEventDelegate onBeginDrag = null; public PointerEventDelegate onDrag = null; public PointerEventDelegate onEndDrag = null; public PointerEventDelegate onDrop = null; public AxisEventDelegate onMove = null; public PointerEventDelegate onClick = null; public PointerEventDelegate onDown = null; public PointerEventDelegate onEnter = null; public PointerEventDelegate onExit = null; public PointerEventDelegate onUp = null; public PointerEventDelegate onScroll = null; public BaseEventDelegate onSelect = null; public BaseEventDelegate onUpdateSelect = null; public BaseEventDelegate onCancel = null; public PointerEventDelegate onInitializePotentialDrag = null; public BaseEventDelegate onSubmit = null; private static PointerEventData mPointData = null; // 设置参数 public void SetData(string key , object val) { mArg[key] = val; } // 获取参数 public D GetData<D>(string key) { if(mArg.ContainsKey(key)) { return (D)mArg[key]; } return default(D); } ... public static UI_Event Get(GameObject go) { UI_Event listener = go.GetComponent<UI_Event>(); if (listener == null) listener = go.AddComponent<UI_Event>(); return listener; } public override void OnBeginDrag( PointerEventData eventData ) { ... } public override void OnDrag( PointerEventData eventData ) { ... } public override void OnEndDrag( PointerEventData eventData ) { ... } public override void OnDrop( PointerEventData eventData ) { ... } public override void OnMove( AxisEventData eventData ) { ... } public override void OnPointerClick(PointerEventData eventData) { ... if(onClick != null) { onClick(eventData , this); } ... } public override void OnPointerDown (PointerEventData eventData) { ... } public override void OnPointerEnter (PointerEventData eventData) { ... } public override void OnPointerExit (PointerEventData eventData) { ... } public override void OnPointerUp (PointerEventData eventData) { ... } public override void OnScroll( PointerEventData eventData ) { ... } }
以上代码只把事件响应最重要的部分,其余还包括组件的挂在、事件的调用及参数的设置等。
最好事先把 UI_Event 挂载到 GameObject 节点上,这样可节省 AddComponent 的消耗,如果要在按钮响应时加入参数,则可再使用 SetData 来设置当前节点的回调参数。
为什么要这么做?
- 统一管理所有事件句柄
- 更加方便地使用输入事件句柄
- 更方便地设置参数
4、自定义组件
(1)UI 动画组件
- 首先它需要依赖 Unity3D 的 Animator 组件 [RequireComponent (typeof(Animator))]
其次它要有播放(Play)接口用来播放指定动画,这里 Play 的参数包括动画名、播放完毕后的回调函数委托等。
再次在 public 变量中需要 AutoPlay 这个参数,这样美术人员就可以在 Unity3D 界面上设置自动播放而无需程序调用了。
最后需要在自动播放时选择指定的动画名和是否循环播放,以及循环播放间隔。
(2)按钮播放音效组件
(3)UI 元素跟随 3D 物体组件
比如游戏中的血条、场景中建筑物头上的标志等。通过不断地计算 3D 物体在屏幕中的位置来确定 UI 位置,当前位置不同时再进行更改以避免不必要的移动。
(4)无限滚动页面组件
比如背包界面。用一个自定义的无线滚动页面组价来替换原来的模式。
(5)其他组件
包括数字飘字组件、计数组件、下拉框组件等。
编写自定义的 UI 组件的目标就是,增加更多通用的组件,减少重复劳动,让程序员在编写 UI 界面时更加快捷、高效,同时也可提升 UI 的运行效率。拥有属于自己的一套自定义套件,对项目来说,也是一件非常有价值和高效的事。