UI (3) - Analysis of input and event modules in UGUI source code

Before understanding the entire UGUI source code, we start with the input event in this article and analyze the input event module in the UGUI source code.

Analysis of UGUI source code

The source code of UGUI is officially released by Unity3D. Here we analyze the open source code of UGUI in Unity2017.

The picture above is the folder structure diagram of the UGUI kernel source code. It divides UGUI into three parts, input events, animation, and core rendering.

Among them, the animation part is relatively simple, using the form of tween tween animation, and the color, position, and size are gradually operated. The principle of tween is to start a coroutine. In the coroutine, the attribute of the element is gradually modified. In addition to modifying the attribute value, tween also has a variety of curves to choose from, such as inner curve, outer curve, etc. A value from The process from the start point to the end point can be controlled by the curve. For example, the change of the number from 0 to 100 is completed in 3 seconds. If it is linear, the value at 2 seconds should be

    (100 - 0) * (2f/3f) = 200f/3f = 66.666

And if you use Neiban curves, the result will not be the same, but they will eventually reach 100, but the process is a bit 'twisted', and the curves also reflect the 'interesting' of the animation.

Let's focus on analyzing the two parts of input events and core rendering.

Enter event source code

The file structure diagram of the input event source code is as follows:

In the figure, UGUI divides the input event module into four parts, event data module, input event capture module, ray collision detection module, event logic processing and callback module. Let's pull out the core source code of each part and analyze it.

event data module

In terms of the function of the event data module to the entire event system, it mainly defines and stores the location when the event occurs, the object corresponding to the event, the displacement of the event, the input type that triggers the event, and the device information of the event, etc. The event data module does not do too much content logically, but mainly to obtain data and provide data services.

It has three classes PointerEventData, AxisEventData, and BaseEventData, which are point event data class, wheel event data class, and event basic data class. PointerEventData and AxisEventData inherit from BaseEventData, and the amount of code of AxisEventData is very small, because it only needs to provide the direction information of the scroll wheel. That is as follows:

namespace UnityEngine.EventSystems
{
    public class AxisEventData : BaseEventData
    {
        //移动方向
        public Vector2 moveVector { get; set; }
        public MoveDirection moveDir { get; set; }

        public AxisEventData(EventSystem eventSystem)
            : base(eventSystem)
        {
            moveVector = Vector2.zero;
            moveDir = MoveDirection.None;
        }
    }
}

BaseEventData 定义了几个常用的接口,其子类 PointerEventData 是最常用的事件数据,我们来看看它是如何编写的,代码量并不多基本全是数据定义:


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

    // 接收OnPointerDown事件的物体
    private GameObject m_PointerPress;
    // 上一下接收OnPointerDown事件的物体
    public GameObject lastPress { get; private set; }
    // 接收按下事件的无法响应处理的物体
    public GameObject rawPointerPress { get; set; }
    // 接收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; }

    // 鼠标或触摸时的点位
    public Vector2 position { get; set; }
    // 滚轮的移速
    public Vector2 delta { get; set; }
    // 按下时的点位
    public Vector2 pressPosition { get; set; }
    
    // 为双击服务的上次点击时间
    public float clickTime { get; set; }
    // 为双击服务的点击次数
    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,它存储了大部分的事件系统逻辑需要的数据,包括按下时的位置,松开与按下的时间差,拖动的位移差,点击到的物体等等,承载了所有输入事件需要的数据。事件数据模块的意义所在便是存储数据并为逻辑部分做好准备。

事件数据模块,主要作用为在各种事件发生时,为事件逻辑做好数据工作。

输入事件捕获模块源码

缺图UGUI的时间捕获模块文件夹结构

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

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

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

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

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

下面我们来看看他们的核心部分的代码:


/// <summary>
/// 处理所有的鼠标事件
/// </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 的主函数 ProcessMouseEvent,它从鼠标键盘输入事件上扩展了输入的逻辑,处理了鼠标的按下,移动,滚轮,拖拽的操作事件。其中比较重要的函数为 ProcessMousePress、ProcessMove、ProcessDrag 这三个函数,我们来重点看下他们处理的内容。


/// <summary>
/// Process the current mouse press.
/// 处理鼠标按下事件
/// </summary>
protected void ProcessMousePress(MouseButtonEventData data)
{
    var pointerEvent = data.buttonData;
    var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

    // PointerDown notification
    // 按下通知
    if (data.PressedThisFrame())
    {
        pointerEvent.eligibleForClick = true;
        pointerEvent.delta = Vector2.zero;
        pointerEvent.dragging = false;
        pointerEvent.useDragThreshold = true;
        pointerEvent.pressPosition = pointerEvent.position;
        pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

        DeselectIfSelectionChanged(currentOverGo, pointerEvent);

        // 搜索元件中按下事件的句柄,并执行按下事件句柄
        var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);

        // didnt find a press handler... search for a click handler
        // 搜索后找不到句柄,就设置一个自己的
        if (newPressed == null)
            newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        // Debug.Log("Pressed: " + newPressed);

        float time = Time.unscaledTime;

        if (newPressed == pointerEvent.lastPress)
        {
            var diffTime = time - pointerEvent.clickTime;
            if (diffTime < 0.3f)
                ++pointerEvent.clickCount;
            else
                pointerEvent.clickCount = 1;

            pointerEvent.clickTime = time;
        }
        else
        {
            pointerEvent.clickCount = 1;
        }

        pointerEvent.pointerPress = newPressed;
        pointerEvent.rawPointerPress = currentOverGo;

        pointerEvent.clickTime = time;

        // Save the drag handler as well
        // 保存拖拽信息
        pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

        // 执行拖拽启动事件句柄
        if (pointerEvent.pointerDrag != null)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
    }

    // PointerUp notification
    // 抬起通知
    if (data.ReleasedThisFrame())
    {
        //执行抬起事件的句柄
        // Debug.Log("Executing pressup on: " + pointer.pointerPress);
        ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

        // Debug.Log("KeyCode: " + pointer.eventData.keyCode);

        var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        // 如果抬起时与按下时为同一个元素,那就是点击
        if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
        {
            ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
        }
        // 否则也可能是拖拽的释放
        else if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
        {
            ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
        }

        pointerEvent.eligibleForClick = false;
        pointerEvent.pointerPress = null;
        pointerEvent.rawPointerPress = null;

        // 如果正在拖拽则抬起事件等于拖拽结束事件
        if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

        pointerEvent.dragging = false;
        pointerEvent.pointerDrag = null;
        
        // 如果当前接收事件的物体和事件的刚开始的物体不一致,则对两个物体做进和出的事件处理
        if (currentOverGo != pointerEvent.pointerEnter)
        {
            HandlePointerExitAndEnter(pointerEvent, null);
            HandlePointerExitAndEnter(pointerEvent, currentOverGo);
        }
    }
}

上面展示了 ProcessMousePress 处理鼠标按下事件的代码,虽然比较多但并不复杂,我在代码上做了详尽的注解。其实它不仅仅处理的是按下的操作,也同时处理鼠标抬起的操作,以及处理了拖拽启动和拖拽抬起与结束的事件。在调用处理相关句柄的前后,事件数据都会被保存在 pointerEvent 中,然后被传递给业务层中设置的输入事件句柄。

我们再来看看 ProcessDrag 拖拽处理函数:


protected virtual void ProcessDrag(PointerEventData pointerEvent)
{
    bool moving = pointerEvent.IsPointerMoving();

    // 如果已经在移动,且还没开始拖拽启动事件,则调用拖拽启动句柄,并设置拖拽中标记为true
    if (moving && pointerEvent.pointerDrag != null
        && !pointerEvent.dragging
        && ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold))
    {
        ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler);
        pointerEvent.dragging = true;
    }

    // 拖拽时的句柄处理
    if (pointerEvent.dragging && moving && pointerEvent.pointerDrag != null)
    {
        // 如果按下的物体和拖拽的物体不是同一个则视为抬起拖拽,并清除前面按下时的标记
        if (pointerEvent.pointerPress != pointerEvent.pointerDrag)
        {
            ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

            pointerEvent.eligibleForClick = false;
            pointerEvent.pointerPress = null;
            pointerEvent.rawPointerPress = null;
        }

        // 执行拖拽中句柄
        ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.dragHandler);
    }
}

上面展示了 ProcessDrag 拖拽句柄处理函数,与ProcessMousePress类似对拖拽事件逻辑做了判断,包括拖拽开始事件处理,判断结束拖拽事件,以及拖拽句柄的调用。

ProcessMove 则相对简单点,每帧都会直接调用处理句柄。


protected virtual void ProcessMove(PointerEventData pointerEvent)
{
    var targetGO = pointerEvent.pointerCurrentRaycast.gameObject;
    HandlePointerExitAndEnter(pointerEvent, targetGO);
}

除了鼠标事件外,我们再来看看触屏事件的处理方式,即 TouchInputModule 的核心函数。如下:


/// <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);
    }
}

从代码中我们看到 ProcessMove 和 ProcessDrag 与前面鼠标事件处理时一样的,只是按下的时间处理不同,而且它对每个触点都做了相同的操作处理。其实 ProcessTouchPress 和鼠标按下处理函数 ProcessMousePress 非常相似,可以说基本上一模一样,只是传入时的数据类型不同而已,由于篇幅有限这里不再重复展示长串代码。

这里大量用到了 ExecuteEvents.ExecuteHierarchy,ExecuteEvents.Execute 之类的静态函数来执行句柄,它是怎么工作的呢,其实很简单:


private static readonly List<Transform> s_InternalTransformList = new List<Transform>(30);

public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler
{
    // 获取物体的所有父节点,包括它自己
    GetEventChain(root, s_InternalTransformList);

    for (var i = 0; i < s_InternalTransformList.Count; i++)
    {
        var transform = s_InternalTransformList[i];
        // 对每个父节点包括自己依次执行句柄响应
        if (Execute(transform.gameObject, eventData, callbackFunction))
            return transform.gameObject;
    }
    return null;
}

解释下上述代码,对所有父节点都调用句柄函数。也就是说,当前节点的事件会通知给它上面的父节点。

到这里我们基本清楚事件处理的基本逻辑了,下面我们来看看碰撞测试模块是如何运作的
射线碰撞检测模块源码

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

射线碰撞检测模块主要为3个类,分别作用于 2D射线碰撞检测,3D射线碰撞检测,GraphicRaycaster图形射线碰撞测试。

2D、3D射线碰撞测试相对比较简单,用射线的形式做碰撞测试,区别在2D碰撞结果里预留了2D的层级次序以便在后面的碰撞结果排序时,以这个层级次序为依据做排序,而3D的碰撞检测结果则是以距离大小为依据排序的。

GraphicRaycaster 为UGUI元素点位检测的类,它被放在了 Core 渲染块里。它主要针对 ScreenSpaceOverlay 模式下输入点位做碰撞检测,因为这个模式下的检测并不依赖于射线碰撞,而是遍历所有可点击的UGUI元素来检测比较,从而判断是该响应哪个UI元素。因此 GraphicRaycaster 是比较特殊的。

我们来着重看下 GraphicRaycaster 的核心源码如下:


/// <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源码地址

Guess you like

Origin blog.csdn.net/s178435865/article/details/129530648