《Unity3D高级编程之进阶主程》第四章,UI(三) - 剖析UGUI源码中的输入与事件模块

前文回顾 UI(一)

    对NGUI和UGUI进行了比较,讲述了如何选择UI系统作为项目的UI框架。

前文回顾 UI(二)

    UGUI的原理,以及组件使用详解。

此篇我们来对UGUI的源码中的输入事件与模块进行剖析。

===

UGUI源码剖析

UGUI的源码是Unity3D官方公开的,这里我们来剖析一下UGUI在Unity2017中的源码部分。

ugui文件夹

这是UGUI内核源码的文件夹结构图。它把UGUI分成了,三块,输入事件,动画,核心渲染。

其中动画部分相对比较简单,用了tween补间动画的形式,对颜色,位置,大小进行了渐进操作。

我们重点来剖析下输入事件和核心渲染两块。

输入事件源码

输入事件源码的整个文件结构图如下:

ugui事件文件夹结构

UGUI输入事件模块有四部分构成,事件数据模块,输入事件捕获模块,射线碰撞检测模块,事件逻辑处理及回调模块。

事件数据模块

事件数据模块部分,主要定义和存储事件发生时的点位,事件对应的物体,事件的位移大小,事件的类型,以及事件的设备类型等。

public class PointerEventData : BaseEventData
{
    public GameObject pointerEnter { get; set; }

    // The object that received OnPointerDown
    private GameObject m_PointerPress;
    // The object last received OnPointerDown
    public GameObject lastPress { get; private set; }
    // The object that the press happened on even if it can not handle the press event
    public GameObject rawPointerPress { get; set; }
    // The object that received OnDrag
    public GameObject pointerDrag { get; set; }

    public RaycastResult pointerCurrentRaycast { get; set; }
    public RaycastResult pointerPressRaycast { get; set; }

    public List<GameObject> hovered = new List<GameObject>();

    public bool eligibleForClick { get; set; }

    public int pointerId { get; set; }

    // Current position of the mouse or touch event
    public Vector2 position { get; set; }
    // Delta since last update
    public Vector2 delta { get; set; }
    // Position of the press event
    public Vector2 pressPosition { get; set; }
    // World-space position where a ray cast into the screen hits something
    [Obsolete("Use either pointerCurrentRaycast.worldPosition or pointerPressRaycast.worldPosition")]
    public Vector3 worldPosition { get; set; }
    // World-space normal where a ray cast into the screen hits something
    [Obsolete("Use either pointerCurrentRaycast.worldNormal or pointerPressRaycast.worldNormal")]
    public Vector3 worldNormal { get; set; }
    // The last time a click event was sent out (used for double-clicks)
    public float clickTime { get; set; }
    // Number of clicks in a row. 2 for a double-click for example.
    public int clickCount { get; set; }

    public Vector2 scrollDelta { get; set; }
    public bool useDragThreshold { get; set; }
    public bool dragging { get; set; }

    public InputButton button { get; set; }
}

事件数据模块,主要用途是在各种事件发生时,或者两个前后事件发生后,对相应数据的存储,为事件逻辑做好数据工作。

数据类的主要核心是 PointerEventData 类,它存储了大部分的逻辑需要的数据,包括按下时的位置,松开与按下的时间差,拖动的位移差等等,几乎承载了所有数据。这是事件数据模块部分的意义所在,存储数据并为逻辑部分做好准备。

输入事件捕获模块源码

输入事件捕获模块由四个类组成,BaseInputModule,PointerInputModule,StandaloneInputModule,TouchInputModule。

BaseInputModule 是抽象(abstract)基类,提供必须的空接口和基本变量。

PointerInputModule 继承了BaseInputModule,并且在他基础上扩展了关于点位的输入逻辑,也增加了输入的类型和状态。

StandaloneInputModule 和 TouchInputModule 继承了 PointerInputModule,它们从父类开始延展向不同的方向。

StandaloneInputModule 向标准键盘鼠标输入方向拓展,而 TouchInputModule 向触控板输入方向拓展。

下面是他们的核心代码部分:

/// <summary>
/// Process all mouse events.
/// </summary>
protected void ProcessMouseEvent(int id)
{
    var mouseData = GetMousePointerEventData(id);
    var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;

    // Process the first mouse button fully
    ProcessMousePress(leftButtonData);
    ProcessMove(leftButtonData.buttonData);
    ProcessDrag(leftButtonData.buttonData);

    // Now process right / middle clicks
    ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
    ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
    ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
    ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);

    if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
    {
        var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
        ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
    }
}

以上 StandaloneInputModule 从鼠标键盘输入事件上扩展了输入的逻辑。检测并处理了鼠标的按下,移动,拖拽的操作。

/// <summary>
/// Process all touch events.
/// </summary>
private void ProcessTouchEvents()
{
    for (int i = 0; i < Input.touchCount; ++i)
    {
        Touch input = Input.GetTouch(i);

        bool released;
        bool pressed;
        var pointer = GetTouchPointerEventData(input, out pressed, out released);

        ProcessTouchPress(pointer, pressed, released);

        if (!released)
        {
            ProcessMove(pointer);
            ProcessDrag(pointer);
        }
        else
            RemovePointerData(pointer);
    }
}

以上 TouchInputModule 则从触控板输入上进行扩展。检测并处理了,触控板手指按下,移动拖拽的操作。

射线碰撞检测模块源码

射线碰撞检测模块主要工作是从摄像头指定的位置上,做射线碰撞检测,并获取碰撞结果。再把结果返回给事件处理逻辑类,由事件处理逻辑处理事件。

射线碰撞检测部分总共4个类,主要是 2D 碰撞检测,3D 碰撞检测,以及UGUI元素点位检测三个方面的检测。

用 Physics2D 还是 Physics 区别了2D 和 3D 的碰撞检测,在2D碰撞结果里预留了2D的层级次序以便在后面的碰撞结果排序时,以这个层级次序为依据做排序,而3D的碰撞检测结果则是以距离大小为依据排序的。

另外有一个不在这个文件夹里的一个 UGUI元素点位检测 的类 GraphicRaycaster ,它被放在了 Core 渲染块的文件夹里。

它主要针对 ScreenSpaceOverlay 模式下输入点位检测,这个模式下的检测并不依赖于射线,而是遍历所有可点击的UGUI元素来检查和计算,判断是该响应哪个UI元素。

核心源码如下:

/// <summary>
/// Perform a raycast into the screen and collect all graphics underneath it.
/// </summary>
[NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, List<Graphic> results)
{
    // Debug.Log("ttt" + pointerPoision + ":::" + camera);
    // Necessary for the event system
    var foundGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
    for (int i = 0; i < foundGraphics.Count; ++i)
    {
        Graphic graphic = foundGraphics[i];

        // -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
        if (graphic.depth == -1 || !graphic.raycastTarget)
            continue;

        if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
            continue;

        if (graphic.Raycast(pointerPosition, eventCamera))
        {
            s_SortedGraphics.Add(graphic);
        }
    }

    s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
    //      StringBuilder cast = new StringBuilder();
    for (int i = 0; i < s_SortedGraphics.Count; ++i)
        results.Add(s_SortedGraphics[i]);
    //      Debug.Log (cast.ToString());

    s_SortedGraphics.Clear();
}

上述 GraphicRaycaster 类中的代码,对每个可以点击的元素(raycastTarget是否为true,并且 depth 不为-1)进行检测点位是否落在该元素上。

再通过 depth 变量排序,判断最先落在哪个元素上,从而确定哪个元素响应输入事件。

检测碰撞模块主要的数据结构是 RaycastResult 类,这里承载了所有碰撞检测结果的依据。包括了距离,世界点位,屏幕点位,2D层级次序,碰撞物体等,为后面事件处理提供了数据上的依据。

事件逻辑处理模块

事件主逻辑处理模块,主要的逻辑都集中在 EventSystem 类中,其余的类都是对它起辅助作用的。

EventInterfaces,EventTrigger,EventTriggerType 定义了事件回调函数,ExecuteEvents 编写了所有执行事件的回调接口。

EventSystem 主逻辑里总共300行代码基本上都在处理由射线碰撞检测后引起的各类事件,判断事件是否成立,成立则发起事件回调,不成立则继续轮询检查,等待相关事件的发生。

EventSystem 是事件处理模块中唯一继承 MonoBehavior 并且有在 Update 帧循环中做轮询的。也就是说,所有UI事件的发生都是通过 EventSystem 轮询监测到的并且实施的。EventSystem 通过调用输入事件检测模块,检测碰撞模块,来形成自己主逻辑部分。

所以 EventSystem 是主线逻辑,是整个事件模块的主心骨。架构师在架构时已经将整个事件层各自的职能拆分的很清楚,所以我们看起来也并没有那么难。

输入监测已经由输入事件捕捉模块完成,碰撞检测由碰撞检测模块完成,事件的数据类都有各自的定义,EventSystem 要做的就只是把它们拼装起来成为主逻辑块。

UGUI源码地址

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    《Unity3D高级编程之进阶主程》第四章,UI(三) - 剖析UGUI源码中的输入与事件模块

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号