4.3.4 输入事件捕获模块源码

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

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

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

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

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

它们的核心部分代码如下:


///<summary>
/// 处理所有的鼠标事件
///</summary>
protected void ProcessMouseEvent(int id)
{
    var mouseData = GetMousePointerEventData(id); // 通过id获取鼠标事件数据
    // 再通过鼠标数据获取鼠标左键的事件数据
    var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.
        Left).eventData;
    // 处理鼠标左键相关的事件
    ProcessMousePress(leftButtonData);
    ProcessMove(leftButtonData.buttonData);
    ProcessDrag(leftButtonData.buttonData);

    // 处理鼠标右键和中键的点击事件
    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>(left
            ButtonData.buttonData.pointerCurrentRaycast.gameObject);
        ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, 
            ExecuteEvents.scrollHandler);
    }
}

以上代码为StandaloneInputModule类的主函数ProcessMouseEvent()的代码,它从鼠标键盘输入事件上扩展了输入的逻辑,处理鼠标的按下、移动、滚轮、拖曳等操作事件。其中比较重要的函数为ProcessMousePress()、ProcessMove()、ProcessDrag()这三个函数,我们来重点看看它们处理的内容,其源码如下:


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

    // 按下通知
    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);

        // 搜索后找不到句柄,就设置一个自己的句柄
        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;
        // 保存拖曳信息
        pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>
            (currentOverGo);
        // 执行拖曳启动事件句柄
        if (pointerEvent.pointerDrag != null)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, 
                ExecuteEvents.initializePotentialDrag);
    }

    // 鼠标或手指松开事件
    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<IPointerClick
            Handler>(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()与前面的鼠标事件处理是一样的,只是按下的事件处理不同,而且它对每个触点都执行了相同的操作。其实Process-TouchPress()和鼠标按下处理函数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;
}

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

至此我们基本清楚事件处理的基本逻辑了,下面来看看碰撞测试模块是如何运作的。