《Unity3D高级编程 主程手记》第四章 用户界面(五) 快速构建一个简单易用的 UI 框架

从宏观角度看 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 来设置当前节点的回调参数。

        为什么要这么做?

  1. 统一管理所有事件句柄
  2. 更加方便地使用输入事件句柄
  3. 更方便地设置参数

4、自定义组件

(1)UI 动画组件

  • 首先它需要依赖 Unity3D 的 Animator 组件 [RequireComponent (typeof(Animator))]
  • 其次它要有播放(Play)接口用来播放指定动画,这里 Play 的参数包括动画名、播放完毕后的回调函数委托等。

  • 再次在 public 变量中需要 AutoPlay 这个参数,这样美术人员就可以在 Unity3D 界面上设置自动播放而无需程序调用了。

  • 最后需要在自动播放时选择指定的动画名和是否循环播放,以及循环播放间隔。

(2)按钮播放音效组件

(3)UI 元素跟随 3D 物体组件

        比如游戏中的血条、场景中建筑物头上的标志等。通过不断地计算 3D 物体在屏幕中的位置来确定 UI 位置,当前位置不同时再进行更改以避免不必要的移动。

(4)无限滚动页面组件

        比如背包界面。用一个自定义的无线滚动页面组价来替换原来的模式。

(5)其他组件

        包括数字飘字组件、计数组件、下拉框组件等。

        编写自定义的 UI 组件的目标就是,增加更多通用的组件,减少重复劳动,让程序员在编写 UI 界面时更加快捷、高效,同时也可提升 UI 的运行效率。拥有属于自己的一套自定义套件,对项目来说,也是一件非常有价值和高效的事。

  • 16
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值