《Unity3D高级编程 主程手记》第四章 用户界面(三) UGUI 事件模块剖析

目录

UGUI 事件系统源码剖析

输入事件源码

事件数据模块

BaseEventData

AxisEventData

 PointerEventData 

输入事件捕获模块源码

StandaloneInputModule 的主函数 ProcessMouseEvent() 

ProcessMousePress

ProcessDrag 

ProcessMove

 TouchInputModule 的核心函数

ExecuteEvents.ExecuteHierarchy

射线碰撞检测模块源码

GraphicRaycaster 的核心源码

事件逻辑处理模块


本篇中使用的源码是 Unity 2017 版本。Unity UGUI 公开源码地址

UGUI 事件系统源码剖析

        它把 UGUI 系统分为输入事件,动画,核心渲染三部分。

        动画部分:用了 tween 补间动画的形式,对颜色,位置,大小做了渐进的操作。

        tween 的原理:在启动一个协程,在协程里对元素的属性渐进式的修改,除了修改属性数值,tween还有多种曲线可以选择。

输入事件源码

        UGUI 系统把输入事件模块分为四部分,即事件数据模块、输入事件捕获模块、射线碰撞检测模块、事件逻辑处理及回调模块。

事件数据模块

        作用:它主要定义并且存储了事件发生时的位置、与事件对应的物体、事件的位移大小、触发事件的输入类型及事件的设备信息等。主要是为了获取数据,提供数据服务

        PointerEventData 和 AxisEventData 继承自 BaseEventData。

BaseEventData

        事件基础数据类,定义了几个常用的接口。

public abstract class AbstractEventData
    {
        protected bool m_Used;

        public virtual void Reset()
        {
            m_Used = false;
        }

        public virtual void Use()
        {
            m_Used = true;
        }

        public virtual bool used
        {
            get { return m_Used; }
        }
    }

    public class BaseEventData : AbstractEventData
    {
        private readonly EventSystem m_EventSystem;
        public BaseEventData(EventSystem eventSystem)
        {
            m_EventSystem = eventSystem;
        }

        public BaseInputModule currentInputModule
        {
            get { return m_EventSystem.currentInputModule; }
        }

        public GameObject selectedObject
        {
            get { return m_EventSystem.currentSelectedGameObject; }
            set { m_EventSystem.SetSelectedGameObject(value, this); }
        }
    }

AxisEventData

        滚轮事件数据类,只需要提供滚轮的方向信息。

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

 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 存储了大部分的事件系统逻辑需要的数据,包括按下时的位置、松开与按下的时间差、拖曳的位移差、点击的物体等,承载了所有输入事件需要的数据。

输入事件捕获模块源码

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

  • BaseInputModule 是抽象(abstract)基类,提供必须的空接口和基本变量
  • PointerInputModule 继承自 BaseInputModule,并且在其基础上扩展了关于点位的输入逻辑,增加了输入的类型和状态。
  • StandaloneInputModule TouchInputModule 又继承了 PointerInputModule,它们从父类开始延展向不同的方向。
  • StandaloneInputModule:标准键盘鼠标输入方向拓展。
  • TouchInputModule:触控板输入方向拓展。

StandaloneInputModule 的主函数 ProcessMouseEvent() 

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

        它从鼠标键盘输入事件上扩展了输入的逻辑,处理了鼠标的按下、移动、滚轮、拖曳等操作事件。其中比较重要的函数为 ProcessMousePress()、ProcessMove()、ProcessDrag() 这三个函数。

ProcessMousePress

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

        它不仅仅处理的是按下的操作,还处理了鼠标松开时的操作,以及拖曳启动和拖曳松开与结束的事件。在调用处理相关句柄的前后,事件数据都会保存在 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);
    }
}

        与 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

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

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

射线碰撞检测模块源码

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

        主要包含三个类,分别作用于 2D射线碰撞检测、3D射线碰撞检测GraphicRaycaster 图形射线碰撞检测

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

        GraphicRaycaster 为 UGUI 元素点位检测的类,它被放在了 Core 渲染块里。它主要针对 Screen Space-Overlay 模式下输入点位进行碰撞检测,因为这个模式下的检测并不依赖于射线碰撞,而是遍历所有可点击的 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 主要作用是把这些模块拼装起来成为主逻辑块。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值