UGUI事件模块剖析

UGUI事件系统源码剖析

图4-1为UGUI内核源码的文件夹结构图。
它把UGUI系统分为输入事件、动画、核心渲染三部分。

其中动画部分相对比较简单,采用tween补间动画的形式对颜色、位置、大小进行了渐进的操作。
tween的原理是启动一个协程,在协程里对元素的属性进行渐进式修改,除了修改属性数值,tween还设置了多种曲线以供选择,比如内翻曲线、外翻曲线等,一个数值从起点到终点的过程可以通过曲线来控制。
例如,数字从0到100的变化可在3秒内完成,如果是线性,则在第2秒时的数值应该如下:
(100-0) x (2f/3f) =200f/3f=66.666
如果使用内翻曲线就不是这个结果了,但它们最终都会到达100,只是过程有点“曲折”,曲线也体现了动画的“有趣“。

下面重点剖析输入事件和核心渲染这两部分

输入事件源码


由图4-2可知,UGUI系统将输入事件模块分为四部分,即事件数据模块、输入事件捕获模块、射线碰撞检测模块、事件逻辑处理及回调模块。

下面将分析每一部分的核心源码。

事件数据模块

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

事件数据模块包含PointerEventData、AxisEventData、BaseEventData三个类,分别为点位事件数据类、滚轮事件数据类、事件基础数据类。PointerEventData类和AxisEventData类继承自BaseEventData类,且AxisEventData类的逻辑量非常少,因为它只需要提供滚轮的方向信息。

其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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是最常用的事件数据。
PointerEventData类的代码量并不多,基本全是数据定义,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class PointerEventData : BaseEventData
{
public GameObject pointEnter { 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为数据类的核心类,它存储了大部分事件系统逻辑需要的数据,包括按下时的位置、松开与按下的时间差、拖曳的位移差、点击的物体等,承载了所有输入事件需要的数据。
事件数据模块的意义是存储数据并为逻辑部分做好准备。
事件数据模块的主要作用是在各种事件发生时,为事件逻辑做好数据工作。

输入事件捕获模块源码

输入事件捕获模块由BaselnputModule、PointerlnputModule、StandalonelnputModule、TouchInputModule四个类组成。

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
       /// <summary>
/// Process all mouse events.
/// 处理所有的鼠标事件
/// </summary>
protected void ProcessMouseEvent(int id)
{
// 通过id获取鼠标事件数据
var mouseData = GetMousePointerEventData(id);
// 再通过鼠标数据获取鼠标左键的事件数据
var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;

m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
        /// <summary>
/// Calculate and process any mouse button
/// state changes.
/// 处理鼠标按下事件
/// </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);

// search for the control that will receive the press
// if we can't find a press handler set the press
// handler to be what would receive a click.
// 搜索元件中按下事件的句柄,并执行按下事件句柄
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);

m_InputPointerEvent = pointerEvent;
}

// PointerUp notification
// 鼠标或手指松开事件
if (data.ReleasedThisFrame())
{
ReleaseMouse(pointerEvent, currentOverGo);
}
}

private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
{
// 执行鼠标或手指松开事件的句柄
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

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

// PointerClick and Drop events
// 如果鼠标或手指松开时与按下时为同一个元素,那就是点击
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;

// redo pointer enter / exit to refresh state
// so that if we moused over something that ignored it before
// due to having pressed on something else
// it now gets it.
// 如果当前接收事件的物体和事件刚开始时的物体不一致,则对两个物体做进和出的事件处理。
if (currentOverGo != pointerEvent.pointerEnter)
{
HandlePointerExitAndEnter(pointerEvent, null);
HandlePointerExitAndEnter(pointerEvent, currentOverGo);
}

m_InputPointerEvent = pointerEvent;
}

上面展示了ProcessMousePress()函数处理鼠标按下事件的代码,虽然比较多但并不复杂,我在代码上做了详细注解。该函数不仅处理了鼠标按下的操作,还处理了鼠标松开时的操作,以及拖曳启动和拖曳松开与结束的事件。在调用处理相关句柄的前后,事件数据都会保存在pointerEvent类中,然后被传递给业务层中设置的输入事件句柄。

我们再来看看ProcessDrag()拖曳处理函数,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
     /// <summary>
/// Process the drag for the current frame with the given pointer event.
/// </summary>
protected virtual void ProcessDrag(PointerEventData pointerEvent)
{
if (!pointerEvent.IsPointerMoving() ||
Cursor.lockState == CursorLockMode.Locked ||
pointerEvent.pointerDrag == null)
return;

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

// Drag notification
// 拖曳时的句柄处理
if (pointerEvent.dragging)
{
// Before doing drag we should cancel any pointer down state
// And clear selection!
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()则相对比较简单,每顿都会直接调用处理句柄,其源码如下:

1
2
3
4
5
6
7
8
/// <summary>
/// Process movement for the current frame with the given pointer event.
/// </summary>
protected virtual void ProcessMove(PointerEventData pointerEvent)
{
var targetGO = (Cursor.lockState == CursorLockMode.Locked ? null : pointerEvent.pointerCurrentRaycast.gameObject);
HandlePointerExitAndEnter(pointerEvent, targetGO);
}

除了鼠标事件外,还有触屏事件的处理方式,即TouchlnputModule0的核心函数,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
      /// <summary>
/// Process all touch events.
/// 处理所有触屏事件
/// </summary>
private void ProcessTouchEvents()
{
for (int i = 0; i < input.touchCount; ++i)
{
Touch touch = input.GetTouch(i);

if (touch.type == TouchType.Indirect)
continue;

bool released;
bool pressed;
var pointer = GetTouchPointerEventData(touch, 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()之类的静态函数来执行句柄,它们是怎么工作的呢?其实很简单,源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
     /// <summary>
/// Execute the specified event on the first game object underneath the current touch.
/// </summary>
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渲染块里。它主要针对ScreenSpaceOverlay模式下的输入点位进行碰撞检测,因为这个模式下的检测并不依赖于射线碰撞,而是通过遍历所有可点击的UGUI元素来进行检测比较,从而判断该响应哪个UI元素的。因此GraphicRaycaster类是比较特殊的。

GraphicRaycaster类的核心源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
      /// <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, IList<Graphic> foundGraphics, List<Graphic> results)
{
// Necessary for the event system
// 事件系统所必须的
int totalCount = foundGraphics.Count;
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];

// -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
// -1表示画布尚未对其进行处理,这意味着它实际上并未绘制
if (graphic.depth == -1 || !graphic.raycastTarget || graphic.canvasRenderer.cull)
continue;

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

if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
continue;

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

s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
totalCount = s_SortedGraphics.Count;
for (int i = 0; i < totalCount; ++i)
results.Add(s_SortedGraphics[i]);

s_SortedGraphics.Clear();
}

上述代码中,GraphicRaycaster()对每个可以点击的元素 (raycastTarget是否为true,并且depth不为-1,为可点击元素)进行计算,判断点位是否落在该元素上。再通过depth变量排序,判断最先落在哪个元素上,从而确定哪个元素响应输入事件。

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

事件逻辑处理模块

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

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

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

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

架构者在设计时将整个事件层各自的职能拆分得很清楚,使源码看起来并没有那么难。输入监测由输入事件捕捉模块完成,碰撞检测由碰撞检测模块完成,事件的数据类都有各自的定义,EventSystem类的主要作用是把这些模块拼装起来成为主逻辑块。