EventSystem

Unity 的 UGUI 系统中的 EventSystem 是一个核心组件,用于管理和处理用户输入事件。它是所有 UI 交互的基础,负责检测和分发输入事件,如鼠标点击、触摸、键盘输入等。EventSystem 的主要功能包括:

  1. 管理所有的输入检测模块(InputModule)。 EventSystem 使用输入模块(Input Modules)来处理不同类型的输入并逐帧调用 Module 的执行函数 Process(),常见的输入模块包括 StandaloneInputModule(用于鼠标和键盘输入)和 TouchInputModule(用于触摸输入)。

  2. 调动射线捕捉模块(Raycasters),为 InputModule 提供结果(具体的触点所穿透的对象信息)。 EventSystem 使用射线检测(Raycasting)来确定用户输入的位置和目标。它会发射一条射线,从输入设备(如鼠标或触摸点)的位置出发,检测与之相交的 UI 元素,并将事件传递给 UI 元素。

  3. 事件监听及处理(ExecuteEvent)。 EventSystem 会捕获用户的输入事件,生成对应的 EventData 并将这些事件分发给相应的 UI 元素。UI 元素可以通过实现特定的接口(如 IPointerClickHandlerIPointerEnterHandler 等)来监听和处理事件。EventSystem 会根据事件类型调用相应的接口方法。

EventSystem 中主要的类图如下:

image.png

下面会依次分析这些类的实现及他们之前的依赖关系。

EventSystem

EventSystem 的部分代码如下,这里只保留了主要的逻辑部分,让我们来看一看 EventSystem 内部的实现。

点击展开代码
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
/// <summary>
/// Handles input, raycasting, and sending events.
/// </summary>
/// <remarks>
/// The EventSystem is responsible for processing and handling events in a Unity scene. A scene should only contain one EventSystem.
/// The EventSystem works in conjunction with a number of modules and mostly just holds state and delegates functionality to specific, overrideable components.
/// When the EventSystem is started it searches for any BaseInputModules attached to the same GameObject and adds them to an internal list.
/// On update each attached module receives an UpdateModules call, where the module can modify internal state.
/// After each module has been Updated the active module has the Process call executed.This is where custom module processing can take place.
/// </remarks>
public class EventSystem : UIBehaviour
{
private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();

private BaseInputModule m_CurrentInputModule;

private static List<EventSystem> m_EventSystems = new List<EventSystem>();

/// <summary>
/// Return the current EventSystem.
/// </summary>
public static EventSystem current
{
get { return m_EventSystems.Count > 0 ? m_EventSystems[0] : null; }
set
{
int index = m_EventSystems.IndexOf(value);

if (index >= 0)
{
m_EventSystems.RemoveAt(index);
m_EventSystems.Insert(0, value);
}
}
}

protected EventSystem()
{}

/// <summary>
/// Recalculate the internal list of BaseInputModules.
/// </summary>
public void UpdateModules()
{
GetComponents(m_SystemInputModules);
for (int i = m_SystemInputModules.Count - 1; i >= 0; i--)
{
if (m_SystemInputModules[i] && m_SystemInputModules[i].IsActive())
continue;

m_SystemInputModules.RemoveAt(i);
}
}

/// <summary>
/// Set the object as selected. Will send an OnDeselect the the old selected object and OnSelect to the new selected object.
/// </summary>
/// <param name="selected">GameObject to select.</param>
/// <param name="pointer">Associated EventData.</param>
public void SetSelectedGameObject(GameObject selected, BaseEventData pointer)
{
if (m_SelectionGuard)
{
Debug.LogError("Attempting to select " + selected + "while already selecting an object.");
return;
}

m_SelectionGuard = true;
if (selected == m_CurrentSelected)
{
m_SelectionGuard = false;
return;
}

// Debug.Log("Selection: new (" + selected + ") old (" + m_CurrentSelected + ")");
ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.deselectHandler);
m_CurrentSelected = selected;
ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.selectHandler);
m_SelectionGuard = false;
}

private BaseEventData m_DummyData;
private BaseEventData baseEventDataCache
{
get
{
if (m_DummyData == null)
m_DummyData = new BaseEventData(this);

return m_DummyData;
}
}

/// <summary>
/// Set the object as selected. Will send an OnDeselect the the old selected object and OnSelect to the new selected object.
/// </summary>
/// <param name="selected">GameObject to select.</param>
public void SetSelectedGameObject(GameObject selected)
{
SetSelectedGameObject(selected, baseEventDataCache);
}

private static int RaycastComparer(RaycastResult lhs, RaycastResult rhs)
{
if (lhs.module != rhs.module)
{
var lhsEventCamera = lhs.module.eventCamera;
var rhsEventCamera = rhs.module.eventCamera;
if (lhsEventCamera != null && rhsEventCamera != null && lhsEventCamera.depth != rhsEventCamera.depth)
{
// need to reverse the standard compareTo
if (lhsEventCamera.depth < rhsEventCamera.depth)
return 1;
if (lhsEventCamera.depth == rhsEventCamera.depth)
return 0;

return -1;
}

if (lhs.module.sortOrderPriority != rhs.module.sortOrderPriority)
return rhs.module.sortOrderPriority.CompareTo(lhs.module.sortOrderPriority);

if (lhs.module.renderOrderPriority != rhs.module.renderOrderPriority)
return rhs.module.renderOrderPriority.CompareTo(lhs.module.renderOrderPriority);
}

if (lhs.sortingLayer != rhs.sortingLayer)
{
// Uses the layer value to properly compare the relative order of the layers.
var rid = SortingLayer.GetLayerValueFromID(rhs.sortingLayer);
var lid = SortingLayer.GetLayerValueFromID(lhs.sortingLayer);
return rid.CompareTo(lid);
}

if (lhs.sortingOrder != rhs.sortingOrder)
return rhs.sortingOrder.CompareTo(lhs.sortingOrder);

// comparing depth only makes sense if the two raycast results have the same root canvas (case 912396)
if (lhs.depth != rhs.depth && lhs.module.rootRaycaster == rhs.module.rootRaycaster)
return rhs.depth.CompareTo(lhs.depth);

if (lhs.distance != rhs.distance)
return lhs.distance.CompareTo(rhs.distance);

return lhs.index.CompareTo(rhs.index);
}

private static readonly Comparison<RaycastResult> s_RaycastComparer = RaycastComparer;

/// <summary>
/// Raycast into the scene using all configured BaseRaycasters.
/// </summary>
/// <param name="eventData">Current pointer data.</param>
/// <param name="raycastResults">List of 'hits' to populate.</param>
public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
{
raycastResults.Clear();
var modules = RaycasterManager.GetRaycasters();
for (int i = 0; i < modules.Count; ++i)
{
var module = modules[i];
if (module == null || !module.IsActive())
continue;

module.Raycast(eventData, raycastResults);
}

raycastResults.Sort(s_RaycastComparer);
}

protected override void OnEnable()
{
base.OnEnable();
m_EventSystems.Add(this);
}

protected override void OnDisable()
{
if (m_CurrentInputModule != null)
{
m_CurrentInputModule.DeactivateModule();
m_CurrentInputModule = null;
}

m_EventSystems.Remove(this);

base.OnDisable();
}

private void TickModules()
{
for (var i = 0; i < m_SystemInputModules.Count; i++)
{
if (m_SystemInputModules[i] != null)
m_SystemInputModules[i].UpdateModule();
}
}

protected virtual void Update()
{
if (current != this)
return;
TickModules();

bool changedModule = false;
for (var i = 0; i < m_SystemInputModules.Count; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported() && module.ShouldActivateModule())
{
if (m_CurrentInputModule != module)
{
ChangeEventModule(module);
changedModule = true;
}
break;
}
}

// no event module set... set the first valid one...
if (m_CurrentInputModule == null)
{
for (var i = 0; i < m_SystemInputModules.Count; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported())
{
ChangeEventModule(module);
changedModule = true;
break;
}
}
}

if (!changedModule && m_CurrentInputModule != null)
m_CurrentInputModule.Process();
}

private void ChangeEventModule(BaseInputModule module)
{
if (m_CurrentInputModule == module)
return;

if (m_CurrentInputModule != null)
m_CurrentInputModule.DeactivateModule();

if (module != null)
module.ActivateModule();
m_CurrentInputModule = module;
}
}

首先,可以从注释中了解到该类的主要作用:

  • 类的功能:EventSystem 负责处理输入、射线检测(raycasting)和发送事件。

  • 备注:EventSystem 负责在 Unity 场景中处理和管理事件。一个场景中应该只包含一个 EventSystemEventSystem 与多个模块协同工作,主要是保存状态并将功能委托给特定的、可重写的组件。当 EventSystem 启动时,它会搜索附加到同一个 GameObject 的所有 BaseInputModules,并将它们添加到一个内部列表中。在每次更新时,每个附加的模块都会接收到一个 UpdateModules 调用,模块可以在此调用中修改内部状态。在每个模块更新之后,活动模块会执行 Process 调用,这是自定义模块处理的地方。

1
2
3
4
5
6
7
8
9
10
/// <summary>
/// Handles input, raycasting, and sending events.
/// </summary>
/// <remarks>
/// The EventSystem is responsible for processing and handling events in a Unity scene. A scene should only contain one EventSystem.
/// The EventSystem works in conjunction with a number of modules and mostly just holds state and delegates functionality to specific, overrideable components.
/// When the EventSystem is started it searches for any BaseInputModules attached to the same GameObject and adds them to an internal list.
/// On update each attached module receives an UpdateModules call, where the module can modify internal state.
/// After each module has been Updated the active module has the Process call executed.This is where custom module processing can take place.
/// </remarks>

首先,类中定义了三个变量,m_SystemInputModules 用于存储所有的输入模块,m_CurrentInputModule 代表当前处理的输入模块,m_EventSystems 用于存储所有的 EventSystem 实例。

需要注意的是 m_EventSystems 是一个静态变量,用于存储所有的 EventSystem 实例。current 属性用于获取当前的 EventSystem 实例,如果有多个 EventSystem 实例,只返回第一个实例。current 属性的 set 方法用于设置当前的 EventSystem 实例,如果已经存在该实例,则将其移动到列表的第一个位置。这个过程有点类似于 LRU 缓存算法,保证最近使用的 EventSystem 实例在列表的最前面。

此外,EventSystem 的构造函数是 protected 的,这意味着不能直接实例化 EventSystem 对象,只能通过继承 EventSystem 类来创建新的 EventSystem 实例。

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
private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();

private BaseInputModule m_CurrentInputModule;

private static List<EventSystem> m_EventSystems = new List<EventSystem>();

/// <summary>
/// Return the current EventSystem.
/// </summary>
public static EventSystem current
{
get { return m_EventSystems.Count > 0 ? m_EventSystems[0] : null; }
set
{
int index = m_EventSystems.IndexOf(value);

if (index >= 0)
{
m_EventSystems.RemoveAt(index);
m_EventSystems.Insert(0, value);
}
}
}

protected EventSystem()
{}

UpdateModules 方法用于获取所有的输入模块至 m_SystemInputModules,并更新模块列表。UpdateModules 方法会遍历所有的输入模块,如果模块存在且处于激活状态,则保留,否则从列表中移除。

该函数并没有在 EventSystem 的生命周期中被调用,而是在 InputModuleOnEnable()OnDisable() 方法中调用,用于更新输入模块列表。后面会详细介绍 InputModule 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
/// Recalculate the internal list of BaseInputModules.
/// </summary>
public void UpdateModules()
{
GetComponents(m_SystemInputModules);
for (int i = m_SystemInputModules.Count - 1; i >= 0; i--)
{
if (m_SystemInputModules[i] && m_SystemInputModules[i].IsActive())
continue;

m_SystemInputModules.RemoveAt(i);
}
}

接下来是 SetSelectedGameObject 方法,用于设置选中的游戏对象。该方法会发送 OnDeselect 事件给旧的选中对象,发送 OnSelect 事件给新的选中对象。SetSelectedGameObject 方法有两个重载,前者接受一个额外的 BaseEventData 参数,用于传递事件数据;后者只接受一个 GameObject 参数,会使用 baseEventDataCache 方法传递一个默认的 BaseEventData 对象。

为了不重复创建 BaseEventData 对象,EventSystem 类中定义了一个 m_DummyData 变量,用于缓存 BaseEventData 对象。baseEventDataCache 方法用于获取 m_DummyData 对象,如果对象为空,则创建一个新的 BaseEventData 对象。这种做法可以减少内存分配的开销。

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
/// <summary>
/// Set the object as selected. Will send an OnDeselect the the old selected object and OnSelect to the new selected object.
/// </summary>
/// <param name="selected">GameObject to select.</param>
/// <param name="pointer">Associated EventData.</param>
public void SetSelectedGameObject(GameObject selected, BaseEventData pointer)
{
if (m_SelectionGuard)
{
Debug.LogError("Attempting to select " + selected + "while already selecting an object.");
return;
}

m_SelectionGuard = true;
if (selected == m_CurrentSelected)
{
m_SelectionGuard = false;
return;
}

// Debug.Log("Selection: new (" + selected + ") old (" + m_CurrentSelected + ")");
ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.deselectHandler);
m_CurrentSelected = selected;
ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.selectHandler);
m_SelectionGuard = false;
}

private BaseEventData m_DummyData;
private BaseEventData baseEventDataCache
{
get
{
if (m_DummyData == null)
m_DummyData = new BaseEventData(this);

return m_DummyData;
}
}

/// <summary>
/// Set the object as selected. Will send an OnDeselect the the old selected object and OnSelect to the new selected object.
/// </summary>
/// <param name="selected">GameObject to select.</param>
public void SetSelectedGameObject(GameObject selected)
{
SetSelectedGameObject(selected, baseEventDataCache);
}

接下来是 RaycastAll 方法,用于对场景中的所有 BaseRaycaster 进行射线检测。RaycastAll 方法会清空 raycastResults 列表,然后遍历所有的 BaseRaycaster,调用 Raycaster.Raycast 方法进行射线检测,并将结果保存到 raycastResults 列表中。最后,对 raycastResults 列表进行排序,排序规则由 RaycastComparer 方法定义。

关于 RaycastAll 有以下几个需要注意的地方:

  1. EventSystem 并不直接调用 RaycastAll 方法,而是由 InputModule 调用。InputModule 会在 Process 方法中调用 RaycastAll 方法,获取射线检测的结果。

  2. RaycastComparer 方法用于对射线检测结果进行排序,总体而言就是将更接近摄像机的结果排在前面。

  3. RaycastAll 方法会通过 RaycasterManager.GetRaycasters() 获取所有的 BaseRaycaster,然后遍历调用 Raycaster.Raycast 方法进行射线检测。

关于 InputModuleRaycasterManager 的实现会在下文中详细介绍。

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
private static int RaycastComparer(RaycastResult lhs, RaycastResult rhs)
{
if (lhs.module != rhs.module)
{
var lhsEventCamera = lhs.module.eventCamera;
var rhsEventCamera = rhs.module.eventCamera;
if (lhsEventCamera != null && rhsEventCamera != null && lhsEventCamera.depth != rhsEventCamera.depth)
{
// need to reverse the standard compareTo
if (lhsEventCamera.depth < rhsEventCamera.depth)
return 1;
if (lhsEventCamera.depth == rhsEventCamera.depth)
return 0;

return -1;
}

if (lhs.module.sortOrderPriority != rhs.module.sortOrderPriority)
return rhs.module.sortOrderPriority.CompareTo(lhs.module.sortOrderPriority);

if (lhs.module.renderOrderPriority != rhs.module.renderOrderPriority)
return rhs.module.renderOrderPriority.CompareTo(lhs.module.renderOrderPriority);
}

if (lhs.sortingLayer != rhs.sortingLayer)
{
// Uses the layer value to properly compare the relative order of the layers.
var rid = SortingLayer.GetLayerValueFromID(rhs.sortingLayer);
var lid = SortingLayer.GetLayerValueFromID(lhs.sortingLayer);
return rid.CompareTo(lid);
}

if (lhs.sortingOrder != rhs.sortingOrder)
return rhs.sortingOrder.CompareTo(lhs.sortingOrder);

// comparing depth only makes sense if the two raycast results have the same root canvas (case 912396)
if (lhs.depth != rhs.depth && lhs.module.rootRaycaster == rhs.module.rootRaycaster)
return rhs.depth.CompareTo(lhs.depth);

if (lhs.distance != rhs.distance)
return lhs.distance.CompareTo(rhs.distance);

return lhs.index.CompareTo(rhs.index);
}

private static readonly Comparison<RaycastResult> s_RaycastComparer = RaycastComparer;

/// <summary>
/// Raycast into the scene using all configured BaseRaycasters.
/// </summary>
/// <param name="eventData">Current pointer data.</param>
/// <param name="raycastResults">List of 'hits' to populate.</param>
public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
{
raycastResults.Clear();
var modules = RaycasterManager.GetRaycasters();
for (int i = 0; i < modules.Count; ++i)
{
var module = modules[i];
if (module == null || !module.IsActive())
continue;

module.Raycast(eventData, raycastResults);
}

raycastResults.Sort(s_RaycastComparer);
}

接下来是关于 EventSystem 的生命周期的两个方法,OnEnableOnDisableOnEnable 方法会在 EventSystem 启用时调用,用于将当前 EventSystem 实例添加到 m_EventSystems 列表中。OnDisable 方法会在 EventSystem 禁用时调用,用于将当前的输入模块禁用,并从 m_EventSystems 列表中移除当前 EventSystem 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected override void OnEnable()
{
base.OnEnable();
m_EventSystems.Add(this);
}

protected override void OnDisable()
{
if (m_CurrentInputModule != null)
{
m_CurrentInputModule.DeactivateModule();
m_CurrentInputModule = null;
}

m_EventSystems.Remove(this);

base.OnDisable();
}

最后便是 EventSystem 中最重要的方法 Update,该方法会在每一帧被调用。Update 方法首先会调用 TickModules 方法,遍历所有的输入模块并调用其中的 UpdateModule 方法。

然后会遍历所有的输入模块,并设置当前的输入模块,最后调用当前输入模块的 Process 方法,完成事件的处理。

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
private void TickModules()
{
for (var i = 0; i < m_SystemInputModules.Count; i++)
{
if (m_SystemInputModules[i] != null)
m_SystemInputModules[i].UpdateModule();
}
}

protected virtual void Update()
{
if (current != this)
return;
TickModules();

bool changedModule = false;
for (var i = 0; i < m_SystemInputModules.Count; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported() && module.ShouldActivateModule())
{
if (m_CurrentInputModule != module)
{
ChangeEventModule(module);
changedModule = true;
}
break;
}
}

// no event module set... set the first valid one...
if (m_CurrentInputModule == null)
{
for (var i = 0; i < m_SystemInputModules.Count; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported())
{
ChangeEventModule(module);
changedModule = true;
break;
}
}
}

if (!changedModule && m_CurrentInputModule != null)
m_CurrentInputModule.Process();
}

private void ChangeEventModule(BaseInputModule module)
{
if (m_CurrentInputModule == module)
return;

if (m_CurrentInputModule != null)
m_CurrentInputModule.DeactivateModule();

if (module != null)
module.ActivateModule();
m_CurrentInputModule = module;
}

小结

以上我们对 EventSystem 类的实现进行了详细介绍。总的来看,EventSystem 的主要功能是处理输入、射线检测和发送事件。
首先,挂载了 EventSystem 的 GameObject 会在启动时收集所有的输入模块并添加到 m_SystemInputModules 列表中。然后,EventSystem 会在每一帧调用 Update 方法,遍历所有的输入模块并调用 Process 方法,完成事件的处理。

此外,EventSystem 还提供了一些方法用于设置选中的游戏对象、射线检测等功能。这些方法会在 InputModuleRaycaster 中调用。

image.png

InputModule

BaseInput

在介绍 BaseInputModule 之前需要先了解一下 BaseInput,该类是 BaseInputModule 中处理输入的接口类,封装了许多诸如鼠标位置、滚动、存在状态等的接口,并将这些属性都标记为 virtual,这意味着我们可以派生出自己的 Input 类并重写这些属性,以达到自定义输入的效果。

值得注意的是,BaseInput 本身也只是对 Input 模块的一层封装而已,其中的所有属性都来自于 Input 类中的 static 变量。可以理解为 BaseInput 只是为了 UI 界面的使用方便,将 Unity 自身的 Input 模块中的部分属性拆分出来做了二次封装,本身并不直接参与用户输入的监听及处理。

点击展开代码
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
120
121
122
123
124
125
/// <summary>
/// Interface to the Input system used by the BaseInputModule. With this it is possible to bypass the Input system with your own but still use the same InputModule.
/// For example this can be used to feed fake input into the UI or interface with a different input system.
/// </summary>
public class BaseInput : UIBehaviour
{
/// <summary>
/// Interface to Input.compositionString. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual string compositionString
{
get { return Input.compositionString; }
}

/// <summary>
/// Interface to Input.imeCompositionMode. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual IMECompositionMode imeCompositionMode
{
get { return Input.imeCompositionMode; }
set { Input.imeCompositionMode = value; }
}

/// <summary>
/// Interface to Input.compositionCursorPos. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual Vector2 compositionCursorPos
{
get { return Input.compositionCursorPos; }
set { Input.compositionCursorPos = value; }
}

/// <summary>
/// Interface to Input.mousePresent. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual bool mousePresent
{
get { return Input.mousePresent; }
}

/// <summary>
/// Interface to Input.GetMouseButtonDown. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
/// <param name="button"></param>
/// <returns></returns>
public virtual bool GetMouseButtonDown(int button)
{
return Input.GetMouseButtonDown(button);
}

/// <summary>
/// Interface to Input.GetMouseButtonUp. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual bool GetMouseButtonUp(int button)
{
return Input.GetMouseButtonUp(button);
}

/// <summary>
/// Interface to Input.GetMouseButton. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual bool GetMouseButton(int button)
{
return Input.GetMouseButton(button);
}

/// <summary>
/// Interface to Input.mousePosition. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual Vector2 mousePosition
{
get { return Input.mousePosition; }
}

/// <summary>
/// Interface to Input.mouseScrollDelta. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual Vector2 mouseScrollDelta
{
get { return Input.mouseScrollDelta; }
}

/// <summary>
/// Interface to Input.touchSupported. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual bool touchSupported
{
get { return Input.touchSupported; }
}

/// <summary>
/// Interface to Input.touchCount. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
public virtual int touchCount
{
get { return Input.touchCount; }
}

/// <summary>
/// Interface to Input.GetTouch. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
/// <param name="index">Touch index to get</param>
public virtual Touch GetTouch(int index)
{
return Input.GetTouch(index);
}

/// <summary>
/// Interface to Input.GetAxisRaw. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
/// <param name="axisName">Axis name to check</param>
public virtual float GetAxisRaw(string axisName)
{
return Input.GetAxisRaw(axisName);
}

/// <summary>
/// Interface to Input.GetButtonDown. Can be overridden to provide custom input instead of using the Input class.
/// </summary>
/// <param name="buttonName">Button name to get</param>
public virtual bool GetButtonDown(string buttonName)
{
return Input.GetButtonDown(buttonName);
}
}

这部分代码较多,这里就不一一展开了,我们只需要知道 BaseInputInputModule 提供了一系列的接口,用于获取鼠标位置、滚动、触摸等输入信息。这些接口都是 virtual 的,可以被子类重写,用于自定义输入的处理。而 BaseInput 本身并不直接参与用户输入的监听及处理,只是对 Input 模块的一层封装。

EventData

EventDataInputModule 中使用到的另一个数据类,负责传递 EventSystem 中产生的各种事件数据。EventData 类中定义了一系列的属性和方法,用于存储事件数据,如 currentInputModuleselectedObject 等,BaseEventDataEventData 的基类,派生出了一系列的子类,如 PointerEventDataAxisEventData 等,用于存储不同类型的事件数据。

image.png

  • BaseEventData:基础的事件信息

  • PointerEventData: 存储 触摸/点击/鼠标操作 事件信息

  • AxisEventData:移动相关的事件信息,注:拖动的操作属于 PointerEventData

这里我们只分析一下 BaseEventData 的实现:

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
/// <summary>
/// A class that can be used for sending simple events via the event system.
/// </summary>
public abstract class AbstractEventData
{
protected bool m_Used;

/// <summary>
/// Reset the event.
/// </summary>
public virtual void Reset()
{
m_Used = false;
}

/// <summary>
/// Use the event.
/// </summary>
/// <remarks>
/// Internally sets a flag that can be checked via used to see if further processing should happen.
/// </remarks>
public virtual void Use()
{
m_Used = true;
}

/// <summary>
/// Is the event used?
/// </summary>
public virtual bool used
{
get { return m_Used; }
}
}

/// <summary>
/// A class that contains the base event data that is common to all event types in the new EventSystem.
/// </summary>
public class BaseEventData : AbstractEventData
{
private readonly EventSystem m_EventSystem;
public BaseEventData(EventSystem eventSystem)
{
m_EventSystem = eventSystem;
}

/// <summary>
/// >A reference to the BaseInputModule that sent this event.
/// </summary>
public BaseInputModule currentInputModule
{
get { return m_EventSystem.currentInputModule; }
}

/// <summary>
/// The object currently considered selected by the EventSystem.
/// </summary>
public GameObject selectedObject
{
get { return m_EventSystem.currentSelectedGameObject; }
set { m_EventSystem.SetSelectedGameObject(value, this); }
}
}

首先,定义了一个抽象类 AbstractEventData,其中包含了 ResetUseused 三个方法,用于重置事件、标记事件已使用、判断事件是否已使用。BaseEventData 继承自 AbstractEventData,并额外定义了 m_EventSystemcurrentInputModuleselectedObject 三个属性,用于存储事件系统、当前输入模块、当前选中的游戏对象。

其中,used 属性对于事件的处理非常重要,当事件被处理后,会调用 Use 方法将 m_Used 标记为 true,表示事件已被使用,不再需要进一步处理。在下文的分析中,我们会看到 InputModule 中会根据事件的 used 属性来判断是否需要继续处理事件。

BaseInputModule

BaseInputModule 是所有输入模块的基类,它定义了一系列的虚方法,用于处理用户输入事件。

点击展开代码
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
    [RequireComponent(typeof(EventSystem))]
/// <summary>
/// A base module that raises events and sends them to GameObjects.
/// </summary>
/// <remarks>
/// An Input Module is a component of the EventSystem that is responsible for raising events and sending them to GameObjects for handling.
/// The BaseInputModule is a class that all Input Modules in the EventSystem inherit from. Examples of provided modules are TouchInputModule and StandaloneInputModule,
/// if these are inadequate for your project you can create your own by extending from the BaseInputModule.
/// </remarks>
public abstract class BaseInputModule : UIBehaviour
{
[NonSerialized]
protected List<RaycastResult> m_RaycastResultCache = new List<RaycastResult>();

private AxisEventData m_AxisEventData;

private EventSystem m_EventSystem;
private BaseEventData m_BaseEventData;

protected BaseInput m_InputOverride;
private BaseInput m_DefaultInput;

/// <summary>
/// The current BaseInput being used by the input module.
/// </summary>
public BaseInput input
{
get
{
if (m_InputOverride != null)
return m_InputOverride;

if (m_DefaultInput == null)
{
var inputs = GetComponents<BaseInput>();
foreach (var baseInput in inputs)
{
// We dont want to use any classes that derrive from BaseInput for default.
if (baseInput != null && baseInput.GetType() == typeof(BaseInput))
{
m_DefaultInput = baseInput;
break;
}
}

if (m_DefaultInput == null)
m_DefaultInput = gameObject.AddComponent<BaseInput>();
}

return m_DefaultInput;
}
}

/// <summary>
/// Used to override the default BaseInput for the input module.
/// </summary>
/// <remarks>
/// With this it is possible to bypass the Input system with your own but still use the same InputModule. For example this can be used to feed fake input into the UI or interface with a different input system.
/// </remarks>
public BaseInput inputOverride
{
get { return m_InputOverride; }
set { m_InputOverride = value; }
}

protected EventSystem eventSystem
{
get { return m_EventSystem; }
}

protected override void OnEnable()
{
base.OnEnable();
m_EventSystem = GetComponent<EventSystem>();
m_EventSystem.UpdateModules();
}

protected override void OnDisable()
{
m_EventSystem.UpdateModules();
base.OnDisable();
}

/// <summary>
/// Process the current tick for the module.
/// </summary>
public abstract void Process();

/// <summary>
/// Return the first valid RaycastResult.
/// </summary>
protected static RaycastResult FindFirstRaycast(List<RaycastResult> candidates)
{
for (var i = 0; i < candidates.Count; ++i)
{
if (candidates[i].gameObject == null)
continue;

return candidates[i];
}
return new RaycastResult();
}

/// <summary>
/// Given an input movement, determine the best MoveDirection.
/// </summary>
/// <param name="x">X movement.</param>
/// <param name="y">Y movement.</param>
protected static MoveDirection DetermineMoveDirection(float x, float y)
{
return DetermineMoveDirection(x, y, 0.6f);
}

/// <summary>
/// Given an input movement, determine the best MoveDirection.
/// </summary>
/// <param name="x">X movement.</param>
/// <param name="y">Y movement.</param>
/// <param name="deadZone">Dead zone.</param>
protected static MoveDirection DetermineMoveDirection(float x, float y, float deadZone)
{
// if vector is too small... just return
if (new Vector2(x, y).sqrMagnitude < deadZone * deadZone)
return MoveDirection.None;

if (Mathf.Abs(x) > Mathf.Abs(y))
{
if (x > 0)
return MoveDirection.Right;
return MoveDirection.Left;
}
else
{
if (y > 0)
return MoveDirection.Up;
return MoveDirection.Down;
}
}

/// <summary>
/// Given 2 GameObjects, return a common root GameObject (or null).
/// </summary>
/// <param name="g1">GameObject to compare</param>
/// <param name="g2">GameObject to compare</param>
/// <returns></returns>
protected static GameObject FindCommonRoot(GameObject g1, GameObject g2)
{
if (g1 == null || g2 == null)
return null;

var t1 = g1.transform;
while (t1 != null)
{
var t2 = g2.transform;
while (t2 != null)
{
if (t1 == t2)
return t1.gameObject;
t2 = t2.parent;
}
t1 = t1.parent;
}
return null;
}

// walk up the tree till a common root between the last entered and the current entered is foung
// send exit events up to (but not inluding) the common root. Then send enter events up to
// (but not including the common root).
protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget)
{
// if we have no target / pointerEnter has been deleted
// just send exit events to anything we are tracking
// then exit
if (newEnterTarget == null || currentPointerData.pointerEnter == null)
{
for (var i = 0; i < currentPointerData.hovered.Count; ++i)
ExecuteEvents.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler);

currentPointerData.hovered.Clear();

if (newEnterTarget == null)
{
currentPointerData.pointerEnter = null;
return;
}
}

// if we have not changed hover target
if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget)
return;

GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);

// and we already an entered object from last time
if (currentPointerData.pointerEnter != null)
{
// send exit handler call to all elements in the chain
// until we reach the new target, or null!
Transform t = currentPointerData.pointerEnter.transform;

while (t != null)
{
// if we reach the common root break out!
if (commonRoot != null && commonRoot.transform == t)
break;

ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);
currentPointerData.hovered.Remove(t.gameObject);
t = t.parent;
}
}

// now issue the enter call up to but not including the common root
currentPointerData.pointerEnter = newEnterTarget;
if (newEnterTarget != null)
{
Transform t = newEnterTarget.transform;

while (t != null && t.gameObject != commonRoot)
{
ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);
currentPointerData.hovered.Add(t.gameObject);
t = t.parent;
}
}
}

/// <summary>
/// Given some input data generate an AxisEventData that can be used by the event system.
/// </summary>
/// <param name="x">X movement.</param>
/// <param name="y">Y movement.</param>
/// <param name="deadZone">Dead zone.</param>
protected virtual AxisEventData GetAxisEventData(float x, float y, float moveDeadZone)
{
if (m_AxisEventData == null)
m_AxisEventData = new AxisEventData(eventSystem);

m_AxisEventData.Reset();
m_AxisEventData.moveVector = new Vector2(x, y);
m_AxisEventData.moveDir = DetermineMoveDirection(x, y, moveDeadZone);
return m_AxisEventData;
}

/// <summary>
/// Generate a BaseEventData that can be used by the EventSystem.
/// </summary>
protected virtual BaseEventData GetBaseEventData()
{
if (m_BaseEventData == null)
m_BaseEventData = new BaseEventData(eventSystem);

m_BaseEventData.Reset();
return m_BaseEventData;
}

/// <summary>
/// If the module is pointer based, then override this to return true if the pointer is over an event system object.
/// </summary>
/// <param name="pointerId">Pointer ID</param>
/// <returns>Is the given pointer over an event system object?</returns>
public virtual bool IsPointerOverGameObject(int pointerId)
{
return false;
}

/// <summary>
/// Should the module be activated.
/// </summary>
public virtual bool ShouldActivateModule()
{
return enabled && gameObject.activeInHierarchy;
}

/// <summary>
/// Called when the module is deactivated. Override this if you want custom code to execute when you deactivate your module.
/// </summary>
public virtual void DeactivateModule()
{}

/// <summary>
/// Called when the module is activated. Override this if you want custom code to execute when you activate your module.
/// </summary>
public virtual void ActivateModule()
{}

/// <summary>
/// Update the internal state of the Module.
/// </summary>
public virtual void UpdateModule()
{}

/// <summary>
/// Check to see if the module is supported. Override this if you have a platform specific module (eg. TouchInputModule that you do not want to activate on standalone.)
/// </summary>
/// <returns>Is the module supported.</returns>
public virtual bool IsModuleSupported()
{
return true;
}
}

首先,BaseInputModule 被添加了 RequireComponent 特性,表示 BaseInputModule 必须要附加到一个 EventSystem 上。

通过阅读注释,我们可以了解到 BaseInputModule 是一个基础模块,用于触发事件并将事件发送到游戏对象进行处理。该类是所有输入模块的基类。所有输入模块都继承自这个类。Unity 提供了一些示例模块,如 TouchInputModuleStandaloneInputModule。如果这些示例模块不能满足项目需求,可以通过继承 BaseInputModule 创建自定义的输入模块。

1
2
3
4
5
6
7
8
9
[RequireComponent(typeof(EventSystem))]
/// <summary>
/// A base module that raises events and sends them to GameObjects.
/// </summary>
/// <remarks>
/// An Input Module is a component of the EventSystem that is responsible for raising events and sending them to GameObjects for handling.
/// The BaseInputModule is a class that all Input Modules in the EventSystem inherit from. Examples of provided modules are TouchInputModule and StandaloneInputModule,
/// if these are inadequate for your project you can create your own by extending from the BaseInputModule.
/// </remarks>

接下来是 BaseInputModule 中的定义的一些属性。其中,m_RaycastResultCachem_AxisEventDatam_BaseEventData 分别用于缓存射线检测结果、轴事件数据、基础事件数据。这些属性都是后续的函数中会频繁使用的一些中间变量,这里也是通过缓存的方式来减少内存分配的开销。

其中,input 属性比较有意思,它用于获取当前的 BaseInput 对象。如果 m_InputOverride 不为空,则返回 m_InputOverride;否则,遍历当前 GameObject 上的所有 BaseInput 组件,找到第一个类型为 BaseInput 的组件并返回。如果没有找到,则创建一个新的 BaseInput 组件并返回。

可以看出,m_InputOverride 是可以覆盖默认的 BaseInput 的,这样就可以通过自定义的 BaseInput 来实现自定义的输入处理。同时类中开放了 inputOverride 属性,用于设置 m_InputOverride

此外,通过 eventSystem 属性可以获取当前的 EventSystem 实例。由此可以看出,InputModuleEventSystem 是双向关联的。一方面,EventSystem 会在启用时调用 UpdateModules 方法,更新输入模块列表;另一方面,InputModule 会在启用时获取当前的 EventSystem 实例。

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
[NonSerialized]
protected List<RaycastResult> m_RaycastResultCache = new List<RaycastResult>();

private AxisEventData m_AxisEventData;

private EventSystem m_EventSystem;
private BaseEventData m_BaseEventData;

protected BaseInput m_InputOverride;
private BaseInput m_DefaultInput;

/// <summary>
/// The current BaseInput being used by the input module.
/// </summary>
public BaseInput input
{
get
{
if (m_InputOverride != null)
return m_InputOverride;

if (m_DefaultInput == null)
{
var inputs = GetComponents<BaseInput>();
foreach (var baseInput in inputs)
{
// We dont want to use any classes that derrive from BaseInput for default.
if (baseInput != null && baseInput.GetType() == typeof(BaseInput))
{
m_DefaultInput = baseInput;
break;
}
}

if (m_DefaultInput == null)
m_DefaultInput = gameObject.AddComponent<BaseInput>();
}

return m_DefaultInput;
}
}

/// <summary>
/// Used to override the default BaseInput for the input module.
/// </summary>
/// <remarks>
/// With this it is possible to bypass the Input system with your own but still use the same InputModule. For example this can be used to feed fake input into the UI or interface with a different input system.
/// </remarks>
public BaseInput inputOverride
{
get { return m_InputOverride; }
set { m_InputOverride = value; }
}

protected EventSystem eventSystem
{
get { return m_EventSystem; }
}

接下来是一些虚函数,挑几个重要的分析一下。

  • OnEnableOnDisable 方法用于在 InputModule 启用和禁用时调用,用于更新输入模块列表。OnEnable 方法会获取当前的 EventSystem 实例,并调用 UpdateModules 方法更新输入模块列表;OnDisable 方法会调用 UpdateModules 方法,将当前的输入模块禁用,并从输入模块列表中移除。

  • Process 方法是 BaseInputModule 的核心方法,用于处理当前的输入事件。这里 Process 被标记为了 abstract,表示子类必须实现该方法。Process 方法会在每一帧被调用,用于处理当前的输入事件。

  • ActivateModuleDeactivateModuleUpdateModule 方法分别用于激活、禁用、更新输入模块。这些方法都是虚方法,可以被子类重写。

  • GetAxisEventDataGetBaseEventData 方法用于生成轴事件数据和基础事件数据。

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
protected override void OnEnable()
{
base.OnEnable();
m_EventSystem = GetComponent<EventSystem>();
m_EventSystem.UpdateModules();
}

protected override void OnDisable()
{
m_EventSystem.UpdateModules();
base.OnDisable();
}

/// <summary>
/// Process the current tick for the module.
/// </summary>
public abstract void Process();

/// <summary>
/// Should the module be activated.
/// </summary>
public virtual bool ShouldActivateModule()
{
return enabled && gameObject.activeInHierarchy;
}

/// <summary>
/// Called when the module is deactivated. Override this if you want custom code to execute when you deactivate your module.
/// </summary>
public virtual void DeactivateModule()
{}

/// <summary>
/// Called when the module is activated. Override this if you want custom code to execute when you activate your module.
/// </summary>
public virtual void ActivateModule()
{}

/// <summary>
/// Update the internal state of the Module.
/// </summary>
public virtual void UpdateModule()
{}

/// <summary>
/// Given some input data generate an AxisEventData that can be used by the event system.
/// </summary>
/// <param name="x">X movement.</param>
/// <param name="y">Y movement.</param>
/// <param name="deadZone">Dead zone.</param>
protected virtual AxisEventData GetAxisEventData(float x, float y, float moveDeadZone)
{
if (m_AxisEventData == null)
m_AxisEventData = new AxisEventData(eventSystem);

m_AxisEventData.Reset();
m_AxisEventData.moveVector = new Vector2(x, y);
m_AxisEventData.moveDir = DetermineMoveDirection(x, y, moveDeadZone);
return m_AxisEventData;
}

/// <summary>
/// Generate a BaseEventData that can be used by the EventSystem.
/// </summary>
protected virtual BaseEventData GetBaseEventData()
{
if (m_BaseEventData == null)
m_BaseEventData = new BaseEventData(eventSystem);

m_BaseEventData.Reset();
return m_BaseEventData;
}

接下来是一些工具性的方法,这些方法用于处理输入事件,如射线检测、移动方向的判断等。这些方法大都被标记为 static,表示这些方法是独立于对象的,可以直接通过类名调用。

  • FindFirstRaycast 方法用于返回第一个有效的射线检测结果。一般而言就是确定用户当前点击的是哪个 UI 元素了。

  • DetermineMoveDirection 方法用于根据输入的移动方向,判断最佳的移动方向。这个方法会根据输入的 x、y 值,判断出最佳的移动方向。

  • FindCommonRoot 方法用于找到两个 GameObject 的共同根节点。这个方法会遍历两个 GameObject 的父节点,直到找到共同的根节点。

    值得一提的是,该算法使用的场景有点像 LCA(最近公共祖先)的算法,只不过这里是找到两个 GameObject 的共同根节点。这个算法的时间复杂度是 O(n^2),如果使用 LCA 算法,时间复杂度可以降低到 O(n)。但两个 GameObject 不一定在同一棵树上,因此这种暴力的方法也是可以接受的。

  • HandlePointerExitAndEnter 方法用于处理指针的进入和退出事件。这个方法会根据当前的指针数据和新的进入目标,发送相应的进入和退出事件。该方法会调用 FindCommonRoot 方法找到两个 GameObject 的共同根节点,然后分别沿着父节点向上发送退出和进入事件。可以看到,事件的处理都是调用 ExecuteEvents.Execute 方法来完成,下文中会详细介绍这个方法。

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/// <summary>
/// Return the first valid RaycastResult.
/// </summary>
protected static RaycastResult FindFirstRaycast(List<RaycastResult> candidates)
{
for (var i = 0; i < candidates.Count; ++i)
{
if (candidates[i].gameObject == null)
continue;

return candidates[i];
}
return new RaycastResult();
}

/// <summary>
/// Given an input movement, determine the best MoveDirection.
/// </summary>
/// <param name="x">X movement.</param>
/// <param name="y">Y movement.</param>
protected static MoveDirection DetermineMoveDirection(float x, float y)
{
return DetermineMoveDirection(x, y, 0.6f);
}

/// <summary>
/// Given an input movement, determine the best MoveDirection.
/// </summary>
/// <param name="x">X movement.</param>
/// <param name="y">Y movement.</param>
/// <param name="deadZone">Dead zone.</param>
protected static MoveDirection DetermineMoveDirection(float x, float y, float deadZone)
{
// if vector is too small... just return
if (new Vector2(x, y).sqrMagnitude < deadZone * deadZone)
return MoveDirection.None;

if (Mathf.Abs(x) > Mathf.Abs(y))
{
if (x > 0)
return MoveDirection.Right;
return MoveDirection.Left;
}
else
{
if (y > 0)
return MoveDirection.Up;
return MoveDirection.Down;
}
}

/// <summary>
/// Given 2 GameObjects, return a common root GameObject (or null).
/// </summary>
/// <param name="g1">GameObject to compare</param>
/// <param name="g2">GameObject to compare</param>
/// <returns></returns>
protected static GameObject FindCommonRoot(GameObject g1, GameObject g2)
{
if (g1 == null || g2 == null)
return null;

var t1 = g1.transform;
while (t1 != null)
{
var t2 = g2.transform;
while (t2 != null)
{
if (t1 == t2)
return t1.gameObject;
t2 = t2.parent;
}
t1 = t1.parent;
}
return null;
}

// walk up the tree till a common root between the last entered and the current entered is foung
// send exit events up to (but not inluding) the common root. Then send enter events up to
// (but not including the common root).
protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget)
{
// if we have no target / pointerEnter has been deleted
// just send exit events to anything we are tracking
// then exit
if (newEnterTarget == null || currentPointerData.pointerEnter == null)
{
for (var i = 0; i < currentPointerData.hovered.Count; ++i)
ExecuteEvents.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler);

currentPointerData.hovered.Clear();

if (newEnterTarget == null)
{
currentPointerData.pointerEnter = null;
return;
}
}

// if we have not changed hover target
if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget)
return;

GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);

// and we already an entered object from last time
if (currentPointerData.pointerEnter != null)
{
// send exit handler call to all elements in the chain
// until we reach the new target, or null!
Transform t = currentPointerData.pointerEnter.transform;

while (t != null)
{
// if we reach the common root break out!
if (commonRoot != null && commonRoot.transform == t)
break;

ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);
currentPointerData.hovered.Remove(t.gameObject);
t = t.parent;
}
}

// now issue the enter call up to but not including the common root
currentPointerData.pointerEnter = newEnterTarget;
if (newEnterTarget != null)
{
Transform t = newEnterTarget.transform;

while (t != null && t.gameObject != commonRoot)
{
ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);
currentPointerData.hovered.Add(t.gameObject);
t = t.parent;
}
}
}

PointerInputModule

PointerInputModule 是由 BaseInputModule 派生出来的一个子类,用于处理鼠标、触摸等指针输入。PointerInputModule 中定义了一系列的属性和方法,用于处理指针输入事件。

平心而论,这部分的代码写的还是比较糟糕的,很多地方并没有做到高内聚低耦合。例如 PointerInputModule 定义了一个 protected void CopyFromTo(PointerEventData @from, PointerEventData @to) 方法,用于复制一个 PointerEventData 对象的属性到另一个 PointerEventData 对象中。写过 C++ 的同学应该非常熟悉这个操作,operator= 嘛,因此就更不应该放在这里了,而是应该直接放在 PointerEventData 类中;再比如,由于类中需要维护一个鼠标的所有按键对应的事件,因此定义了一堆类,只为了存放一个 m_MouseState 对象,带来的结果就是可读性非常差。

但既然是学习 UGUI,我们就还是要一点点地分析这部分的实现。这里就不贴完整代码了,直接开始分析。

首先,类中分别为鼠标的三个按键及触摸板定义了一些常量,用于标识这些按键的 ID。此外,类中定义了一个 Dictionary<int, PointerEventData> 类型的字典 m_PointerData,用于缓存当前的按键对应的事件数据。这里也是使用了缓存的设计,提前分配了 m_PointerData 的内存,供后续使用。

GetPointerData 方法用于从缓存中获取当前的事件数据。如果缓存中没有对应的事件数据,则会创建一个新的事件数据,并将其添加到缓存中。RemovePointerData 方法用于从缓存中移除指定的事件数据。

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
/// <summary>
/// Id of the cached left mouse pointer event.
/// </summary>
public const int kMouseLeftId = -1;

/// <summary>
/// Id of the cached right mouse pointer event.
/// </summary>
public const int kMouseRightId = -2;

/// <summary>
/// Id of the cached middle mouse pointer event.
/// </summary>
public const int kMouseMiddleId = -3;

/// <summary>
/// Touch id for when simulating touches on a non touch device.
/// </summary>
public const int kFakeTouchesId = -4;

protected Dictionary<int, PointerEventData> m_PointerData = new Dictionary<int, PointerEventData>();

/// <summary>
/// Search the cache for currently active pointers, return true if found.
/// </summary>
/// <param name="id">Touch ID</param>
/// <param name="data">Found data</param>
/// <param name="create">If not found should it be created</param>
/// <returns>True if pointer is found.</returns>
protected bool GetPointerData(int id, out PointerEventData data, bool create)
{
if (!m_PointerData.TryGetValue(id, out data) && create)
{
data = new PointerEventData(eventSystem)
{
pointerId = id,
};
m_PointerData.Add(id, data);
return true;
}
return false;
}

/// <summary>
/// Remove the PointerEventData from the cache.
/// </summary>
protected void RemovePointerData(PointerEventData data)
{
m_PointerData.Remove(data.pointerId);
}

接下来是一些将触摸输入或鼠标输入转换为对应事件的方法。首先来看 GetTouchPointerEventData 方法。

GetTouchPointerEventData

GetTouchPointerEventData 方法用于将触摸事件转换为 PointerEventData 对象。该方法会根据传入的 input 变量,判断当前是否按下或释放,并将触摸事件的位置、位移、按钮等信息填充到 PointerEventData 对象中。最后,会调用 eventSystem.RaycastAllFindFirstRaycast 方法,获取当前触摸事件对应的射线检测结果,并将其填充到 PointerEventData 对象中。

其中,函数的后两个参数被标记为 out,表示这两个参数是输出参数,用于返回当前触摸事件是否按下或释放。这种设计方式减少了函数的返回值,提高了代码的可读性。

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
/// <summary>
/// Given a touch populate the PointerEventData and return if we are pressed or released.
/// </summary>
/// <param name="input">Touch being processed</param>
/// <param name="pressed">Are we pressed this frame</param>
/// <param name="released">Are we released this frame</param>
/// <returns></returns>
protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
{
PointerEventData pointerData;
var created = GetPointerData(input.fingerId, out pointerData, true);

pointerData.Reset();

pressed = created || (input.phase == TouchPhase.Began);
released = (input.phase == TouchPhase.Canceled) || (input.phase == TouchPhase.Ended);

if (created)
pointerData.position = input.position;

if (pressed)
pointerData.delta = Vector2.zero;
else
pointerData.delta = input.position - pointerData.position;

pointerData.position = input.position;

pointerData.button = PointerEventData.InputButton.Left;

if (input.phase == TouchPhase.Canceled)
{
pointerData.pointerCurrentRaycast = new RaycastResult();
}
else
{
eventSystem.RaycastAll(pointerData, m_RaycastResultCache);

var raycast = FindFirstRaycast(m_RaycastResultCache);
pointerData.pointerCurrentRaycast = raycast;
m_RaycastResultCache.Clear();
}
return pointerData;
}

GetMousePointerEventData

接下来是 GetMousePointerEventData 方法。由于鼠标有三个按键,且每个按键都有按下、释放等状态,为了维护鼠标整体的状态,这里定义了许多辅助函数及辅助的类。让我们依次分析:

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>
/// Copy one PointerEventData to another.
/// </summary>
protected void CopyFromTo(PointerEventData @from, PointerEventData @to)
{
@to.position = @from.position;
@to.delta = @from.delta;
@to.scrollDelta = @from.scrollDelta;
@to.pointerCurrentRaycast = @from.pointerCurrentRaycast;
@to.pointerEnter = @from.pointerEnter;
}

/// <summary>
/// Given a mouse button return the current state for the frame.
/// </summary>
/// <param name="buttonId">Mouse button ID</param>
protected PointerEventData.FramePressState StateForMouseButton(int buttonId)
{
var pressed = input.GetMouseButtonDown(buttonId);
var released = input.GetMouseButtonUp(buttonId);
if (pressed && released)
return PointerEventData.FramePressState.PressedAndReleased;
if (pressed)
return PointerEventData.FramePressState.Pressed;
if (released)
return PointerEventData.FramePressState.Released;
return PointerEventData.FramePressState.NotChanged;
}

CopyFromTo 方法用于将一个 PointerEventData 对象的属性复制到另一个 PointerEventData 对象中。这个方法在 GetMousePointerEventData 方法中会被频繁调用,用于复制当前鼠标按键的状态。

StateForMouseButton 方法用于根据鼠标按键的 ID,返回当前按键的状态。这个方法会调用 input.GetMouseButtonDowninput.GetMouseButtonUp 方法,判断当前按键是否按下或释放。最后,根据按键的状态,返回对应的 PointerEventData.FramePressState 枚举值。

1
private readonly MouseState m_MouseState = new MouseState();

接下来类中维护了一个 MouseState 类型的对象 m_MouseState,用于存储鼠标的状态。MouseState 类型定义了一个 ButtonState 类型的数组,用于存储鼠标的三个按键的状态,并向外提供了获取及设置这些按键状态的接口。这些类之间的依赖关系如下图所示:

mouseState.drawio 1.png

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
/// <summary>
/// Return the current MouseState.
/// </summary>
protected virtual MouseState GetMousePointerEventData(int id)
{
// Populate the left button...
PointerEventData leftData;
var created = GetPointerData(kMouseLeftId, out leftData, true);

leftData.Reset();

if (created)
leftData.position = input.mousePosition;

Vector2 pos = input.mousePosition;
if (Cursor.lockState == CursorLockMode.Locked)
{
// We don't want to do ANY cursor-based interaction when the mouse is locked
leftData.position = new Vector2(-1.0f, -1.0f);
leftData.delta = Vector2.zero;
}
else
{
leftData.delta = pos - leftData.position;
leftData.position = pos;
}
leftData.scrollDelta = input.mouseScrollDelta;
leftData.button = PointerEventData.InputButton.Left;
eventSystem.RaycastAll(leftData, m_RaycastResultCache);
var raycast = FindFirstRaycast(m_RaycastResultCache);
leftData.pointerCurrentRaycast = raycast;
m_RaycastResultCache.Clear();

// copy the apropriate data into right and middle slots
PointerEventData rightData;
GetPointerData(kMouseRightId, out rightData, true);
CopyFromTo(leftData, rightData);
rightData.button = PointerEventData.InputButton.Right;

PointerEventData middleData;
GetPointerData(kMouseMiddleId, out middleData, true);
CopyFromTo(leftData, middleData);
middleData.button = PointerEventData.InputButton.Middle;

m_MouseState.SetButtonState(PointerEventData.InputButton.Left, StateForMouseButton(0), leftData);
m_MouseState.SetButtonState(PointerEventData.InputButton.Right, StateForMouseButton(1), rightData);
m_MouseState.SetButtonState(PointerEventData.InputButton.Middle, StateForMouseButton(2), middleData);

return m_MouseState;
}

分析完了上述的辅助类后,终于可以来看 GetMousePointerEventData 方法了。该方法用于获取当前的鼠标状态。

首先,通过 GetPointerData 方法获取当前鼠标左键对应的事件数据,由上述分析可知,这里使用了缓存的设计。接着,会根据当前的 input 变量,将鼠标的位置、位移、滚轮滚动等信息填充到 PointerEventData 对象中。接下来,会调用 eventSystem.RaycastAllFindFirstRaycast 方法,获取当前鼠标事件对应的射线检测结果,并将其填充到 PointerEventData 对象中。

对于中键和右键,会调用 GetPointerData 方法获取当前鼠标中键和右键对应的事件数据,并调用 CopyFromTo 方法将左键的事件数据复制到中键和右键的事件数据中。最后,会调用 StateForMouseButton 方法,获取当前中键和右键的状态,并调用 m_MouseState.SetButtonState 方法,设置中键和右键的状态。

完成了这些操作后,就可以返回当前的鼠标状态了。这里也使用了缓存的设计,将状态全部更新至 m_MouseState 对象中,供后续使用。

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

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

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

接下来有一些对应事件的处理方法,总体来说都是调用与事件类型对应的 ExecuteEvents.Execute 方法。

到这里我们就分析完了 PointerInputModule 类的基本内容,可以看到,PointerInputModule 类将鼠标、触摸等按键的输入转换为了对应的时间数据,并已经定义了相应的处理方法,看上去已经可以处理用户输入了。

然而,PointerInputModule 类依然是一个抽象类,并没有实现 BaseInputModule 中定义的 Process 方法。因此,还需要继承 PointerInputModule 类,并实现 Process 方法,才能真正处理用户的输入事件。

StandaloneInputModule

image.png

由类图可知,Unity 中自带两个从 PointerInputModule 派生出的处理输入的类:StandaloneInputModuleTouchInputModule。后者已经被禁用,现在所有的输入均由 StandaloneInputModule 一个类处理。从类的注释中我们也可以得到相同的结论:

1
2
3
4
5
6
7
8
[AddComponentMenu("Event/Standalone Input Module")]
/// <summary>
/// A BaseInputModule designed for mouse / keyboard / controller input.
/// </summary>
/// <remarks>
/// Input module for working with, mouse, keyboard, or controller.
/// </remarks>
public class StandaloneInputModule : PointerInputModule

由前文对 EvenySystem 的分析可知,该类会逐帧调用 GameObject 上挂载的 InputModule 中的 UpdateModule()Process() 方法以处理用户输入。因此针对 StandaloneInputModule,我们业主要分析这两部分的实现。

UpdateModule

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
public override void UpdateModule()
{
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
{
if (m_InputPointerEvent != null && m_InputPointerEvent.pointerDrag != null && m_InputPointerEvent.dragging)
{
ReleaseMouse(m_InputPointerEvent, m_InputPointerEvent.pointerCurrentRaycast.gameObject);
}

m_InputPointerEvent = null;

return;
}

m_LastMousePosition = m_MousePosition;
m_MousePosition = input.mousePosition;
}

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

UpdateModule 方法会在每一帧被调用,用于更新鼠标的位置及其他的类内状态。特别地,如果当前的 EventSystem 失去了焦点,且 ShouldIgnoreEventsOnNoFocus 方法返回 true,则会调用 ReleaseMouse 方法释放鼠标。

Process

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public override void Process()
{
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
return;

bool usedEvent = SendUpdateEventToSelectedObject();

// case 1004066 - touch / mouse events should be processed before navigation events in case
// they change the current selected gameobject and the submit button is a touch / mouse button.

// touch needs to take precedence because of the mouse emulation layer
if (!ProcessTouchEvents() && input.mousePresent)
ProcessMouseEvent();

if (eventSystem.sendNavigationEvents)
{
if (!usedEvent)
usedEvent |= SendMoveEventToSelectedObject();

if (!usedEvent)
SendSubmitEventToSelectedObject();
}
}

Process 方法会在每一帧被调用,用于处理当前的输入事件。该方法会首先调用 SendUpdateEventToSelectedObject 方法,发送更新事件给当前选中的对象。接着,会调用 ProcessTouchEvents 方法处理触摸事件,如果触摸事件处理完毕,则会调用 ProcessMouseEvent 方法处理鼠标事件。最后,如果 eventSystem.sendNavigationEventstrue,则会调用 SendMoveEventToSelectedObject 方法发送移动事件给当前选中的对象,以及调用 SendSubmitEventToSelectedObject 方法发送提交事件给当前选中的对象。

这里多次使用了 usedEvent 变量,用于标识当前的事件是否被处理。如果事件被处理,则会将 usedEvent 置为 true,表示当前的事件无需进一步处理。

来看一下 ProcessTouchEventsProcessMouseEvent 方法的实现。

ProcessTouchEvents

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
private bool 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);
}
return input.touchCount > 0;
}

ProcessTouchEvents 方法用于处理触摸事件。该方法会遍历当前的触摸事件,调用 GetTouchPointerEventData 方法将触摸事件转换为 PointerEventData 对象。接着,会调用 ProcessTouchPress 方法处理触摸事件的按下和释放。如果触摸事件未释放,则会调用 ProcessMoveProcessDrag 方法处理触摸事件的移动和拖拽。最后,如果触摸事件已释放,则会调用 RemovePointerData 方法移除对应的事件数据。

有以下几点需要注意:

  1. ProcessTouchEvents 整体使用了一个 for 循环来处理所有的触摸事件,这样可以应对诸如多点触控等复杂的触摸事件。

  2. ProcessTouchEvents 方法中使用了在 PointerInputModule 类中定义的 GetTouchPointerEventData 方法,用于将触摸事件转换为 PointerEventData 对象。此外,ProcessMoveProcessDragRemovePointerData 方法也是在 PointerInputModule 类中定义的,用于处理触摸事件的移动、拖拽及释放当前事件。

  3. ProcessTouchPress 方法用于处理触摸事件的按下和释放。这个方法会根据触摸事件的按下和释放状态,调用 ExecuteEvents.Execute 方法,发送按下和释放事件。

ProcessMouseEvent

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
protected void ProcessMouseEvent()
{
ProcessMouseEvent(0);
}

/// <summary>
/// Process all mouse events.
/// </summary>
protected void ProcessMouseEvent(int 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);
}
}

ProcessMouseEvent 方法用于处理鼠标事件。该方法会首先调用 GetMousePointerEventData 方法获取当前的鼠标状态。接着,会调用 ProcessMousePress 方法处理鼠标事件的按下和释放。如果鼠标事件未释放,则会调用 ProcessMoveProcessDrag 方法处理鼠标事件的移动和拖拽。最后,如果鼠标事件有滚动,则会调用 ExecuteEvents.ExecuteHierarchy 方法,发送滚动事件。

有以下几点需要注意:

  1. ProcessMouseEvent 方法中使用了在 PointerInputModule 类中定义的 GetMousePointerEventData 方法,用于获取当前的鼠标状态。此外,该函数也使用了在 PointerInputModule 类中定义的 ProcessMoveProcessDrag 方法,用于处理鼠标事件的移动和拖拽。

  2. ProcessMouseEvent 方法中调用了 ProcessMousePress 方法,用于处理鼠标事件的按下和释放。这个方法会根据鼠标事件的按下和释放状态,调用 ExecuteEvents.Execute 方法,发送按下和释放事件。

  3. 可以看出除了 ProcessMove 外,ProcessMousePressProcessDrag 都调用了三次,分别处理鼠标的左键、右键和中键。这主要是因为鼠标的移动是一个整体的事件,只需处理一次,而不同按键的按下和释放事件是独立的,因此需要分别处理。

小结

以上我们就分析完了 InputModule 的相关内容。总体而言,InputModule 类主要用于处理用户的输入事件,将用户的输入事件转换为对应的事件数据,并调用 ExecuteEvents.Execute 方法,发送事件给对应的 GameObject

其中,BaseInput 负责存放一次输入的相关数据,包括如鼠标位置、滚动、存在状态等;EventData 封装了一次输入的事件数据,负责在 EventSystem 中传递事件信息。InputModule 负责处理用户的输入事件,将用户的输入事件转换为对应的事件数据,并根据输入的类型(如按下、释放、移动等)调用对应的 ExecuteEvents.Execute 方法,发送事件给对应的 GameObject

距离我们分析完整个 EventSystem 还有两部分内容:获取当前物体的 Raycast 及负责处理各种事件的 ExecuteEvents,让我们继续往下看。

Raycasters

在上文的分析中我们可以多次看到 RaycastAll() 以及 FindFirstRaycast() 方法,这些方法都是使用射线检测来获取当前的物体。下面就来分析射线检测模块的实现。

RaycastResult

BaseInputEventData 类似,射线检测模块也定义了自己的数据结构以用于存储射线检测的结果,这便是 RaycastResult 类。RaycastResult 类的定义如下:

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
/// <summary>
/// A hit result from a BaseRaycaster.
/// </summary>
public struct RaycastResult
{
private GameObject m_GameObject; // Game object hit by the raycast

/// <summary>
/// The GameObject that was hit by the raycast.
/// </summary>
public GameObject gameObject
{
get { return m_GameObject; }
set { m_GameObject = value; }
}

/// <summary>
/// BaseRaycaster that raised the hit.
/// </summary>
public BaseRaycaster module;

/// <summary>
/// Distance to the hit.
/// </summary>
public float distance;

/// <summary>
/// Hit index
/// </summary>
public float index;

/// <summary>
/// Used by raycasters where elements may have the same unit distance, but have specific ordering.
/// </summary>
public int depth;

/// <summary>
/// The SortingLayer of the hit object.
/// </summary>
/// <remarks>
/// For UI.Graphic elements this will be the values from that graphic's Canvas
/// For 3D objects this will always be 0.
/// For 2D objects if a SpriteRenderer is attached to the same object as the hit collider that SpriteRenderer sortingLayerID will be used.
/// </remarks>
public int sortingLayer;

/// <summary>
/// The SortingOrder for the hit object.
/// </summary>
/// <remarks>
/// For Graphic elements this will be the values from that graphics Canvas
/// For 3D objects this will always be 0.
/// For 2D objects if a SpriteRenderer is attached to the same object as the hit collider that SpriteRenderer sortingOrder will be used.
/// </remarks>
public int sortingOrder;

/// <summary>
/// The world position of the where the raycast has hit.
/// </summary>
public Vector3 worldPosition;

/// <summary>
/// The normal at the hit location of the raycast.
/// </summary>
public Vector3 worldNormal;

/// <summary>
/// The screen position from which the raycast was generated.
/// </summary>
public Vector2 screenPosition;

/// <summary>
/// Is there an associated module and a hit GameObject.
/// </summary>
public bool isValid
{
get { return module != null && gameObject != null; }
}

/// <summary>
/// Reset the result.
/// </summary>
public void Clear()
{
gameObject = null;
module = null;
distance = 0;
index = 0;
depth = 0;
sortingLayer = 0;
sortingOrder = 0;
worldNormal = Vector3.up;
worldPosition = Vector3.zero;
screenPosition = Vector2.zero;
}
}

其中存放了射线检测的结果,包括检测到的物体、射线检测的距离、检测到的物体的深度等信息。此外,其中还有一个 module 属性用于存放产生这次结果的射线本身。RaycastResult 类还定义了 Clear 方法,用于清空射线检测的结果。

RaycasterManager

RaycasterManager 类用于管理所有的射线检测器。RaycasterManager 类是一个静态类,定义了一个 List<BaseRaycaster> 类型的列表 m_Raycasters,用于存放所有的射线检测器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
internal static class RaycasterManager
{
private static readonly List<BaseRaycaster> s_Raycasters = new List<BaseRaycaster>();

public static void AddRaycaster(BaseRaycaster baseRaycaster)
{
if (s_Raycasters.Contains(baseRaycaster))
return;

s_Raycasters.Add(baseRaycaster);
}

public static List<BaseRaycaster> GetRaycasters()
{
return s_Raycasters;
}

public static void RemoveRaycasters(BaseRaycaster baseRaycaster)
{
if (!s_Raycasters.Contains(baseRaycaster))
return;
s_Raycasters.Remove(baseRaycaster);
}
}

RaycasterManager 类定义了 AddRaycasterGetRaycastersRemoveRaycasters 方法,用于添加、获取及移除射线检测器。

ReflectionMethodsCache

正如 BaseInput 本身并不直接处理用户的输入,而是通过 Unity 的 Input 模块获取结果;Raycasters 本身也并不直接处理射线检测,而是通过 Physics 模块直接获取到各种类型射线检测的函数。这些函数就存放在 ReflectionMethodsCache 类中。

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
internal class ReflectionMethodsCache
{
public delegate bool Raycast3DCallback(Ray r, out RaycastHit hit, float f, int i);
public delegate RaycastHit2D Raycast2DCallback(Vector2 p1, Vector2 p2, float f, int i);
public delegate RaycastHit[] RaycastAllCallback(Ray r, float f, int i);
public delegate RaycastHit2D[] GetRayIntersectionAllCallback(Ray r, float f, int i);
public delegate int GetRayIntersectionAllNonAllocCallback(Ray r, RaycastHit2D[] results, float f, int i);
public delegate int GetRaycastNonAllocCallback(Ray r, RaycastHit[] results, float f, int i);

// We call Physics.Raycast and Physics2D.Raycast through reflection to avoid creating a hard dependency from
// this class to the Physics/Physics2D modules, which would otherwise make it impossible to make content with UI
// without force-including both modules.
//
// *NOTE* If other methods are required ensure to add [RequiredByNativeCode] to the bindings for that function. It prevents
// the function from being stripped if required. See Dynamics.bindings.cs for examples (search for GraphicRaycaster.cs).
public ReflectionMethodsCache()
{
var raycast3DMethodInfo = typeof(Physics).GetMethod("Raycast", new[] {typeof(Ray), typeof(RaycastHit).MakeByRefType(), typeof(float), typeof(int)});
if (raycast3DMethodInfo != null)
raycast3D = (Raycast3DCallback)Delegate.CreateDelegate(typeof(Raycast3DCallback), raycast3DMethodInfo);

var raycast2DMethodInfo = typeof(Physics2D).GetMethod("Raycast", new[] {typeof(Vector2), typeof(Vector2), typeof(float), typeof(int)});
if (raycast2DMethodInfo != null)
raycast2D = (Raycast2DCallback)Delegate.CreateDelegate(typeof(Raycast2DCallback), raycast2DMethodInfo);

var raycastAllMethodInfo = typeof(Physics).GetMethod("RaycastAll", new[] {typeof(Ray), typeof(float), typeof(int)});
if (raycastAllMethodInfo != null)
raycast3DAll = (RaycastAllCallback)Delegate.CreateDelegate(typeof(RaycastAllCallback), raycastAllMethodInfo);

var getRayIntersectionAllMethodInfo = typeof(Physics2D).GetMethod("GetRayIntersectionAll", new[] {typeof(Ray), typeof(float), typeof(int)});
if (getRayIntersectionAllMethodInfo != null)
getRayIntersectionAll = (GetRayIntersectionAllCallback)Delegate.CreateDelegate(typeof(GetRayIntersectionAllCallback), getRayIntersectionAllMethodInfo);

var getRayIntersectionAllNonAllocMethodInfo = typeof(Physics2D).GetMethod("GetRayIntersectionNonAlloc", new[] { typeof(Ray), typeof(RaycastHit2D[]), typeof(float), typeof(int) });
if (getRayIntersectionAllNonAllocMethodInfo != null)
getRayIntersectionAllNonAlloc = (GetRayIntersectionAllNonAllocCallback)Delegate.CreateDelegate(typeof(GetRayIntersectionAllNonAllocCallback), getRayIntersectionAllNonAllocMethodInfo);

var getRaycastAllNonAllocMethodInfo = typeof(Physics).GetMethod("RaycastNonAlloc", new[] { typeof(Ray), typeof(RaycastHit[]), typeof(float), typeof(int) });
if (getRaycastAllNonAllocMethodInfo != null)
getRaycastNonAlloc = (GetRaycastNonAllocCallback)Delegate.CreateDelegate(typeof(GetRaycastNonAllocCallback), getRaycastAllNonAllocMethodInfo);
}

public Raycast3DCallback raycast3D = null;
public RaycastAllCallback raycast3DAll = null;
public Raycast2DCallback raycast2D = null;
public GetRayIntersectionAllCallback getRayIntersectionAll = null;
public GetRayIntersectionAllNonAllocCallback getRayIntersectionAllNonAlloc = null;
public GetRaycastNonAllocCallback getRaycastNonAlloc = null;

private static ReflectionMethodsCache s_ReflectionMethodsCache = null;

public static ReflectionMethodsCache Singleton
{
get
{
if (s_ReflectionMethodsCache == null)
s_ReflectionMethodsCache = new ReflectionMethodsCache();
return s_ReflectionMethodsCache;
}
}
};

首先,可以看到类中定义了一系列的委托,用于存放不同种类的射线检测函数。

1
2
3
4
5
6
public delegate bool Raycast3DCallback(Ray r, out RaycastHit hit, float f, int i);
public delegate RaycastHit2D Raycast2DCallback(Vector2 p1, Vector2 p2, float f, int i);
public delegate RaycastHit[] RaycastAllCallback(Ray r, float f, int i);
public delegate RaycastHit2D[] GetRayIntersectionAllCallback(Ray r, float f, int i);
public delegate int GetRayIntersectionAllNonAllocCallback(Ray r, RaycastHit2D[] results, float f, int i);
public delegate int GetRaycastNonAllocCallback(Ray r, RaycastHit[] results, float f, int i);

接着,在 ReflectionMethodsCache 类的构造函数中,通过反射获取了 PhysicsPhysics2D 模块中的射线检测函数,并将其赋值给上述定义的委托。这样,ReflectionMethodsCache 类就可以通过委托调用射线检测函数。

在注释中我们可以看到这样做的目的:

1
2
3
4
5
6
// We call Physics.Raycast and Physics2D.Raycast through reflection to avoid creating a hard dependency from
// this class to the Physics/Physics2D modules, which would otherwise make it impossible to make content with UI
// without force-including both modules.
//
// *NOTE* If other methods are required ensure to add [RequiredByNativeCode] to the bindings for that function. It prevents
// the function from being stripped if required. See Dynamics.bindings.cs for examples (search for GraphicRaycaster.cs).

这样做主要是为了避免 Raycasters 类直接依赖于 PhysicsPhysics2D 模块,这样可以使得 Raycasters 类不依赖于 PhysicsPhysics2D 模块,从而使得 Raycasters 类可以独立于 PhysicsPhysics2D 模块运行。此外,如果需要其他的方法,需要确保在绑定函数中添加 [RequiredByNativeCode],以防止这个函数被裁剪。

ReflectionMethodsCache 是一个单例类,通过 Singleton 属性就可以访问到上述反射出的射线检测函数了。

1
2
3
4
5
6
7
8
9
public static ReflectionMethodsCache Singleton
{
get
{
if (s_ReflectionMethodsCache == null)
s_ReflectionMethodsCache = new ReflectionMethodsCache();
return s_ReflectionMethodsCache;
}
}

BaseRaycaster / PhysicsRaycaster / Physics2DRaycaster

BaseRaycaster 类是所有射线检测器的基类,定义了射线检测器的基本属性及方法。BaseRaycaster 类的定义如下:

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
/// <summary>
/// Base class for any RayCaster.
/// </summary>
/// <remarks>
/// A Raycaster is responsible for raycasting against scene elements to determine if the cursor is over them. Default Raycasters include PhysicsRaycaster, Physics2DRaycaster, GraphicRaycaster.
/// Custom raycasters can be added by extending this class.
/// </remarks>
public abstract class BaseRaycaster : UIBehaviour
{
private BaseRaycaster m_RootRaycaster;

/// <summary>
/// Raycast against the scene.
/// </summary>
/// <param name="eventData">Current event data.</param>
/// <param name="resultAppendList">List of hit Objects.</param>
public abstract void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList);

/// <summary>
/// The camera that will generate rays for this raycaster.
/// </summary>
public abstract Camera eventCamera { get; }

// ...

/// <summary>
/// Raycaster on root canvas
/// </summary>
public BaseRaycaster rootRaycaster
{
get
{
if (m_RootRaycaster == null)
{
var baseRaycasters = GetComponentsInParent<BaseRaycaster>();
if (baseRaycasters.Length != 0)
m_RootRaycaster = baseRaycasters[baseRaycasters.Length - 1];
}

return m_RootRaycaster;
}
}

// ...

protected override void OnEnable()
{
base.OnEnable();
RaycasterManager.AddRaycaster(this);
}

protected override void OnDisable()
{
RaycasterManager.RemoveRaycasters(this);
base.OnDisable();
}

protected override void OnCanvasHierarchyChanged()
{
base.OnCanvasHierarchyChanged();
m_RootRaycaster = null;
}

protected override void OnTransformParentChanged()
{
base.OnTransformParentChanged();
m_RootRaycaster = null;
}
}

有以下几点需要注意:

  1. BaseRaycaster 类是一个抽象类,定义了抽象方法 RaycasteventCameraRaycast 方法用于射线检测,eventCamera 属性用于获取射线检测的相机。

  2. BaseRaycaster 会在启用/禁用时调用 RaycasterManagerAddRaycasterRemoveRaycasters 方法,用于将自身添加到/移除出射线检测器列表。

至于 PhysicsRaycasterPhysics2DRaycaster 类,这两个类分别用于进行 3D 及 2D 的射线检测。他们继承自 BaseRaycaster 类,并实现了 RaycasteventCamera 方法。2D 与 3D 分别使用了 Physics2D/Physics 中射线穿透获取交点信息的方法。

这里以 PhysicsRaycaster.Raycast() 为例分析一下射线检测的过程:

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
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
Ray ray = new Ray();
float distanceToClipPlane = 0;
if (!ComputeRayAndDistance(eventData, ref ray, ref distanceToClipPlane))
return;

int hitCount = 0;

if (m_MaxRayIntersections == 0)
{
if (ReflectionMethodsCache.Singleton.raycast3DAll == null)
return;

m_Hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, finalEventMask);
hitCount = m_Hits.Length;
}
else
{
if (ReflectionMethodsCache.Singleton.getRaycastNonAlloc == null)
return;

if (m_LastMaxRayIntersections != m_MaxRayIntersections)
{
m_Hits = new RaycastHit[m_MaxRayIntersections];
m_LastMaxRayIntersections = m_MaxRayIntersections;
}

hitCount = ReflectionMethodsCache.Singleton.getRaycastNonAlloc(ray, m_Hits, distanceToClipPlane, finalEventMask);
}

if (hitCount > 1)
System.Array.Sort(m_Hits, (r1, r2) => r1.distance.CompareTo(r2.distance));

if (hitCount != 0)
{
for (int b = 0, bmax = hitCount; b < bmax; ++b)
{
var result = new RaycastResult
{
gameObject = m_Hits[b].collider.gameObject,
module = this,
distance = m_Hits[b].distance,
worldPosition = m_Hits[b].point,
worldNormal = m_Hits[b].normal,
screenPosition = eventData.position,
index = resultAppendList.Count,
sortingLayer = 0,
sortingOrder = 0
};
resultAppendList.Add(result);
}
}
}

首先,调用了 ComputeRayAndDistance 方法,用于根据 eventData 中点击的位置得到对应的射线,以及射线在前后裁剪平面的距离(关于裁切平面详见MVP矩阵)。

1
2
3
4
5
ray = eventCamera.ScreenPointToRay(eventPosition);
float projectionDirection = ray.direction.z;
distanceToClipPlane = Mathf.Approximately(0.0f, projectionDirection)
? Mathf.Infinity
: Mathf.Abs((eventCamera.farClipPlane - eventCamera.nearClipPlane) / projectionDirection);

此后,会调用 ReflectionMethodsCache.Singleton 中相应的射线检测方法,将结果存放在 m_Hits 中。

1
m_Hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, finalEventMask);

m_Hits 本身为 RaycastHit[] 类型,想要在 EventSystem 中使用,还需要将其转换为 RaycastResult 类型。这里会遍历 m_Hits,将每一个 RaycastHit 转换为 RaycastResult,并添加到 resultAppendList 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (int b = 0, bmax = hitCount; b < bmax; ++b)
{
var result = new RaycastResult
{
gameObject = m_Hits[b].collider.gameObject,
module = this,
distance = m_Hits[b].distance,
worldPosition = m_Hits[b].point,
worldNormal = m_Hits[b].normal,
screenPosition = eventData.position,
index = resultAppendList.Count,
sortingLayer = 0,
sortingOrder = 0
};
resultAppendList.Add(result);
}

这样,PhysicsRaycaster 类就完成了一次射线检测的过程。

小结

image.png

至此,我们就分析完了射线检测模块的相关内容。总体而言,射线检测模块主要用于检测当前鼠标位置下的物体,以及获取该次射线检测的相关信息。RaycastResult 类用于存放射线检测的结果,RaycasterManager 类用于管理所有的射线检测器,ReflectionMethodsCache 类用于通过反射获取射线检测函数,BaseRaycaster 类是所有射线检测器的基类,定义了射线检测器的基本属性及方法,其中有一个抽象方法 Raycast 用于射线检测。而 PhysicsRaycasterPhysics2DRaycaster 类则分别重写了 RaycasteventCamera 方法,用于进行 3D 及 2D 的射线检测。

ExecuteEvents

ExecuteEventsEventSystem 中的最后一部分内容,它的主要功能为执行各种事件,上文中我们多次看到了 ExecuteEvents.Execute 方法,这个方法就是 ExecuteEvents 类中的一个静态方法。ExecuteEvents 类定义了一系列的静态方法,用于执行 EventSystem 中的事件,将事件发送给对应的 GameObject

EventSystemHandler

EventInterfaces.cs 中定义了 EventSystem 中各类事件的接口,最终事件响应的逻辑需要继承这些接口来书写具体的逻辑。

首先,EventInterfaces 中定义了一个公共的接口 IEventSystemHandler,所有 EventSystem 的事件处理接口均需要继承这个接口。

handler 主要分为三种类型:

  • IPointerXXXHandler : 处理鼠标点击和触屏事件

  • IDragXXXXHandler:处理拖拽事件

  • IXXXHandler:处理其他如选择、取消等事件

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
public interface IEventSystemHandler
{
}

public interface IPointerXXXHandler : IEventSystemHandler
{
/// <summary>
/// Use this callback to detect pointer XXX events
/// </summary>
void OnPointerXXX(PointerEventData eventData);
}

public interface IXXXDragHandler : IEventSystemHandler
{
/// <summary>
/// Called by a BaseInputModule when a drag is XXX.
/// </summary>
void OnXXXDrag(PointerEventData eventData);
}

public interface IXXXHandler : IEventSystemHandler
{
/// <summary>
/// Use this callback to detect XXX events.
/// </summary>
void OnXXX(PointerEventData eventData);
}

ExecuteEvents

Handler

ExecuteEvents 类定义了一系列的静态方法,用于执行事件。这些方法主要用于执行 EventSystem 中的事件,将事件发送给对应的 GameObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public delegate void EventFunction<T1>(T1 handler, BaseEventData eventData);

public static T ValidateEventData<T>(BaseEventData data) where T : class
{
if ((data as T) == null)
throw new ArgumentException(String.Format("Invalid type: {0} passed to event expecting {1}", data.GetType(), typeof(T)));
return data as T;
}

private static readonly EventFunction<IPointerEnterHandler> s_PointerEnterHandler = Execute;

private static void Execute(IPointerEnterHandler handler, BaseEventData eventData)
{
handler.OnPointerEnter(ValidateEventData<PointerEventData>(eventData));
}

private static readonly EventFunction<IPointerExitHandler> s_PointerExitHandler = Execute;

private static void Execute(IPointerExitHandler handler, BaseEventData eventData)
{
handler.OnPointerExit(ValidateEventData<PointerEventData>(eventData));
}

/// ...

首先,ExecuteEvents 类定义了一个委托 EventFunction<T1>,该委托接受一个 T1 类型的参数 handler 和一个 BaseEventData 类型的参数 eventData,返回 voidValidateEventData 方法用于验证 eventData 是否为 T 类型,如果不是则抛出异常。

接着,ExecuteEvents 类定义了一系列的静态方法,用于执行 EventSystem 中不同类型的事件,如 PointerEnterPointerExit 等。这些方法都是通过委托调用 handler 的对应方法,将 eventData 传递给 handler

辅助函数

在分析最重要的 Execute 函数之前,先来看一些相关的辅助函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void GetEventChain(GameObject root, IList<Transform> eventChain)
{
eventChain.Clear();
if (root == null)
return;

var t = root.transform;
while (t != null)
{
eventChain.Add(t);
t = t.parent;
}
}

GetEventChain 方法用于获取 GameObject 的事件链。该方法会将 root 的所有父节点添加到 eventChain 中。

1
2
3
4
5
6
7
8
9
10
11
private static bool ShouldSendToComponent<T>(Component component) where T : IEventSystemHandler
{
var valid = component is T;
if (!valid)
return false;

var behaviour = component as Behaviour;
if (behaviour != null)
return behaviour.isActiveAndEnabled;
return true;
}

ShouldSendToComponent 方法用于判断是否应该发送事件给 Component。该方法会判断 component 是否为 T 类型,以及 component 是否激活。如果满足条件,则返回 true,否则返回 false

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
/// <summary>
/// Get the specified object's event event.
/// </summary>
private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler
{
// Debug.LogWarning("GetEventList<" + typeof(T).Name + ">");
if (results == null)
throw new ArgumentException("Results array is null", "results");

if (go == null || !go.activeInHierarchy)
return;

var components = ListPool<Component>.Get();
go.GetComponents(components);
for (var i = 0; i < components.Count; i++)
{
if (!ShouldSendToComponent<T>(components[i]))
continue;

// Debug.Log(string.Format("{2} found! On {0}.{1}", go, s_GetComponentsScratch[i].GetType(), typeof(T)));
results.Add(components[i] as IEventSystemHandler);
}
ListPool<Component>.Release(components);
// Debug.LogWarning("end GetEventList<" + typeof(T).Name + ">");
}

GetEventList 方法用于获取 GameObject 中类型为 T 的所有事件组件。该方法会遍历 go 的所有组件,如果组件为 T 类型且激活,则将其添加到 results 中。

值得注意的是这里在进行中间的 list 变量处理时,使用了 ListPool 类,这是一个对象池,定义在 Core/Utility/ListPool.cs 中,用于减少内存分配。

1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// Whether the specified game object will be able to handle the specified event.
/// </summary>
public static bool CanHandleEvent<T>(GameObject go) where T : IEventSystemHandler
{
var internalHandlers = s_HandlerListPool.Get();
GetEventList<T>(go, internalHandlers);
var handlerCount = internalHandlers.Count;
s_HandlerListPool.Release(internalHandlers);
return handlerCount != 0;
}

CanHandleEvent 方法用于判断 GameObject 是否能够处理 T 类型的事件。该方法会调用 GetEventList 方法获取 GameObject 中类型为 T 的所有事件组件,如果事件组件的数量不为 0,则返回 true,否则返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// Bubble the specified event on the game object, figuring out which object will actually receive the event.
/// </summary>
public static GameObject GetEventHandler<T>(GameObject root) where T : IEventSystemHandler
{
if (root == null)
return null;

Transform t = root.transform;
while (t != null)
{
if (CanHandleEvent<T>(t.gameObject))
return t.gameObject;
t = t.parent;
}
return null;
}

GetEventHandler 方法用于获取 GameObject 中能够处理 T 类型事件的 第一个 事件处理器。该方法会遍历 root 的所有父节点,如果父节点能够处理 T 类型事件,则返回该父节点。

Execute & ExecuteHierarchy

最后,我们来看一下 Execute 方法:

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
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
var internalHandlers = s_HandlerListPool.Get();
GetEventList<T>(target, internalHandlers);
// if (s_InternalHandlers.Count > 0)
// Debug.Log("Executinng " + typeof (T) + " on " + target);

for (var i = 0; i < internalHandlers.Count; i++)
{
T arg;
try
{
arg = (T)internalHandlers[i];
}
catch (Exception e)
{
var temp = internalHandlers[i];
Debug.LogException(new Exception(string.Format("Type {0} expected {1} received.", typeof(T).Name, temp.GetType().Name), e));
continue;
}

try
{
functor(arg, eventData);
}
catch (Exception e)
{
Debug.LogException(e);
}
}

var handlerCount = internalHandlers.Count;
s_HandlerListPool.Release(internalHandlers);
return handlerCount > 0;
}

Execute 方法在 InputModule 部分多次被调用,用于执行 EventSystem 中的不同类型的事件。针对一个特定的输入事件类型 T(如 IPointerEnterHandlerIPointerExitHandler 等),该方法会获取 target 中所有类型为 T 的事件组件,并调用 functor 方法,将 eventData 传递给这些事件组件。

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

ExecuteHierarchy 方法用于在 root 的事件链中执行 T 类型的事件。该方法会获取 root 的事件链,遍历事件链中的每一个节点,并调用 Execute 方法,将 eventData 传递给这些节点。

小结

image.png

至此,我们就分析完了 EventSystem 中的 ExecuteEvents 部分。总体而言,ExecuteEvents 类定义了一系列的静态方法,用于执行 EventSystem 中的事件。这些方法主要用于处理不同类型的 EventSystem 中的事件,并将事件发送给对应的 GameObjectExecuteEvents 类中定义了一系列的辅助函数,用于获取 GameObject 的事件链、获取 GameObject 中类型为 T 的所有事件组件、判断 GameObject 是否能够处理 T 类型事件等;同时,ExecuteEvents 类中定义了 ExecuteExecuteHierarchy 方法,用于执行 EventSystem 中的不同类型的事件。InputModule 中主要就是通过这些方法来执行 EventSystem 中的事件。

总结

image.png

至此,我们就分析完了整个 EventSystem 的实现。EventSystem 是 UGUI 系统中的一个核心组件,用于管理和处理用户输入事件。它是所有 UI 交互的基础,负责检测和分发输入事件,如鼠标点击、触摸、键盘输入等。EventSystem 的主要功能包括:

  1. 管理所有的输入检测模块(InputModule)。 EventSystem 使用输入模块(Input Modules)来处理不同类型的输入并逐帧调用 Module 的执行函数 Process(),常见的输入模块包括 StandaloneInputModule(用于鼠标和键盘输入)和 TouchInputModule(用于触摸输入)。

  2. 调动射线捕捉模块(Raycasters),为 InputModule 提供结果(具体的触点所穿透的对象信息)。 EventSystem 使用射线检测(Raycasting)来确定用户输入的位置和目标。它会发射一条射线,从输入设备(如鼠标或触摸点)的位置出发,检测与之相交的 UI 元素,并将事件传递给 UI 元素。

  3. 事件监听及处理(ExecuteEvent)。 EventSystem 会捕获用户的输入事件,生成对应的 EventData 并将这些事件分发给相应的 UI 元素。UI 元素可以通过实现特定的接口(如 IPointerClickHandlerIPointerEnterHandler 等)来监听和处理事件。EventSystem 会根据事件类型调用相应的接口方法。