Unity 补完计划(四):UGUI-1:EventSystem
EventSystem
Unity 的 UGUI 系统中的 EventSystem
是一个核心组件,用于管理和处理用户输入事件。它是所有 UI 交互的基础,负责检测和分发输入事件,如鼠标点击、触摸、键盘输入等。EventSystem
的主要功能包括:
-
管理所有的输入检测模块(
InputModule
)。EventSystem
使用输入模块(Input Modules)来处理不同类型的输入并逐帧调用 Module 的执行函数Process()
,常见的输入模块包括StandaloneInputModule
(用于鼠标和键盘输入)和TouchInputModule
(用于触摸输入)。 -
调动射线捕捉模块(
Raycasters
),为InputModule
提供结果(具体的触点所穿透的对象信息)。EventSystem
使用射线检测(Raycasting)来确定用户输入的位置和目标。它会发射一条射线,从输入设备(如鼠标或触摸点)的位置出发,检测与之相交的 UI 元素,并将事件传递给 UI 元素。 -
事件监听及处理(
ExecuteEvent
)。EventSystem
会捕获用户的输入事件,生成对应的EventData
并将这些事件分发给相应的 UI 元素。UI 元素可以通过实现特定的接口(如IPointerClickHandler
、IPointerEnterHandler
等)来监听和处理事件。EventSystem
会根据事件类型调用相应的接口方法。
EventSystem 中主要的类图如下:
下面会依次分析这些类的实现及他们之前的依赖关系。
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 场景中处理和管理事件。一个场景中应该只包含一个EventSystem
。EventSystem
与多个模块协同工作,主要是保存状态并将功能委托给特定的、可重写的组件。当EventSystem
启动时,它会搜索附加到同一个 GameObject 的所有BaseInputModules
,并将它们添加到一个内部列表中。在每次更新时,每个附加的模块都会接收到一个UpdateModules
调用,模块可以在此调用中修改内部状态。在每个模块更新之后,活动模块会执行Process
调用,这是自定义模块处理的地方。
1 | /// <summary> |
首先,类中定义了三个变量,m_SystemInputModules
用于存储所有的输入模块,m_CurrentInputModule
代表当前处理的输入模块,m_EventSystems
用于存储所有的 EventSystem
实例。
需要注意的是 m_EventSystems
是一个静态变量,用于存储所有的 EventSystem
实例。current
属性用于获取当前的 EventSystem
实例,如果有多个 EventSystem
实例,只返回第一个实例。current
属性的 set
方法用于设置当前的 EventSystem
实例,如果已经存在该实例,则将其移动到列表的第一个位置。这个过程有点类似于 LRU 缓存算法,保证最近使用的 EventSystem
实例在列表的最前面。
此外,EventSystem
的构造函数是 protected
的,这意味着不能直接实例化 EventSystem
对象,只能通过继承 EventSystem
类来创建新的 EventSystem
实例。
1 | private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>(); |
UpdateModules
方法用于获取所有的输入模块至 m_SystemInputModules
,并更新模块列表。UpdateModules
方法会遍历所有的输入模块,如果模块存在且处于激活状态,则保留,否则从列表中移除。
该函数并没有在 EventSystem
的生命周期中被调用,而是在 InputModule
的 OnEnable()
及 OnDisable()
方法中调用,用于更新输入模块列表。后面会详细介绍 InputModule
的实现。
1 | /// <summary> |
接下来是 SetSelectedGameObject
方法,用于设置选中的游戏对象。该方法会发送 OnDeselect
事件给旧的选中对象,发送 OnSelect
事件给新的选中对象。SetSelectedGameObject
方法有两个重载,前者接受一个额外的 BaseEventData
参数,用于传递事件数据;后者只接受一个 GameObject
参数,会使用 baseEventDataCache
方法传递一个默认的 BaseEventData
对象。
为了不重复创建 BaseEventData
对象,EventSystem
类中定义了一个 m_DummyData
变量,用于缓存 BaseEventData
对象。baseEventDataCache
方法用于获取 m_DummyData
对象,如果对象为空,则创建一个新的 BaseEventData
对象。这种做法可以减少内存分配的开销。
1 | /// <summary> |
接下来是 RaycastAll
方法,用于对场景中的所有 BaseRaycaster
进行射线检测。RaycastAll
方法会清空 raycastResults
列表,然后遍历所有的 BaseRaycaster
,调用 Raycaster.Raycast
方法进行射线检测,并将结果保存到 raycastResults
列表中。最后,对 raycastResults
列表进行排序,排序规则由 RaycastComparer
方法定义。
关于 RaycastAll
有以下几个需要注意的地方:
-
EventSystem
并不直接调用RaycastAll
方法,而是由InputModule
调用。InputModule
会在Process
方法中调用RaycastAll
方法,获取射线检测的结果。 -
RaycastComparer
方法用于对射线检测结果进行排序,总体而言就是将更接近摄像机的结果排在前面。 -
RaycastAll
方法会通过RaycasterManager.GetRaycasters()
获取所有的BaseRaycaster
,然后遍历调用Raycaster.Raycast
方法进行射线检测。
关于 InputModule
及 RaycasterManager
的实现会在下文中详细介绍。
1 | private static int RaycastComparer(RaycastResult lhs, RaycastResult rhs) |
接下来是关于 EventSystem
的生命周期的两个方法,OnEnable
和 OnDisable
。OnEnable
方法会在 EventSystem
启用时调用,用于将当前 EventSystem
实例添加到 m_EventSystems
列表中。OnDisable
方法会在 EventSystem
禁用时调用,用于将当前的输入模块禁用,并从 m_EventSystems
列表中移除当前 EventSystem
实例。
1 | protected override void OnEnable() |
最后便是 EventSystem
中最重要的方法 Update
,该方法会在每一帧被调用。Update
方法首先会调用 TickModules
方法,遍历所有的输入模块并调用其中的 UpdateModule
方法。
然后会遍历所有的输入模块,并设置当前的输入模块,最后调用当前输入模块的 Process
方法,完成事件的处理。
1 | private void TickModules() |
小结
以上我们对 EventSystem
类的实现进行了详细介绍。总的来看,EventSystem
的主要功能是处理输入、射线检测和发送事件。
首先,挂载了 EventSystem
的 GameObject 会在启动时收集所有的输入模块并添加到 m_SystemInputModules
列表中。然后,EventSystem
会在每一帧调用 Update
方法,遍历所有的输入模块并调用 Process
方法,完成事件的处理。
此外,EventSystem
还提供了一些方法用于设置选中的游戏对象、射线检测等功能。这些方法会在 InputModule
及 Raycaster
中调用。
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);
}
}
这部分代码较多,这里就不一一展开了,我们只需要知道 BaseInput
向 InputModule
提供了一系列的接口,用于获取鼠标位置、滚动、触摸等输入信息。这些接口都是 virtual
的,可以被子类重写,用于自定义输入的处理。而 BaseInput
本身并不直接参与用户输入的监听及处理,只是对 Input
模块的一层封装。
EventData
EventData
是 InputModule
中使用到的另一个数据类,负责传递 EventSystem
中产生的各种事件数据。EventData
类中定义了一系列的属性和方法,用于存储事件数据,如 currentInputModule
、selectedObject
等,BaseEventData
是 EventData
的基类,派生出了一系列的子类,如 PointerEventData
、AxisEventData
等,用于存储不同类型的事件数据。
-
BaseEventData
:基础的事件信息 -
PointerEventData
: 存储 触摸/点击/鼠标操作 事件信息 -
AxisEventData
:移动相关的事件信息,注:拖动的操作属于PointerEventData
这里我们只分析一下 BaseEventData
的实现:
1 | /// <summary> |
首先,定义了一个抽象类 AbstractEventData
,其中包含了 Reset
、Use
、used
三个方法,用于重置事件、标记事件已使用、判断事件是否已使用。BaseEventData
继承自 AbstractEventData
,并额外定义了 m_EventSystem
、currentInputModule
、selectedObject
三个属性,用于存储事件系统、当前输入模块、当前选中的游戏对象。
其中,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
[ ]
/// <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
{
[ ]
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 提供了一些示例模块,如 TouchInputModule
和 StandaloneInputModule
。如果这些示例模块不能满足项目需求,可以通过继承 BaseInputModule
创建自定义的输入模块。
1 | [ ] |
接下来是 BaseInputModule
中的定义的一些属性。其中,m_RaycastResultCache
、m_AxisEventData
、m_BaseEventData
分别用于缓存射线检测结果、轴事件数据、基础事件数据。这些属性都是后续的函数中会频繁使用的一些中间变量,这里也是通过缓存的方式来减少内存分配的开销。
其中,input
属性比较有意思,它用于获取当前的 BaseInput
对象。如果 m_InputOverride
不为空,则返回 m_InputOverride
;否则,遍历当前 GameObject 上的所有 BaseInput
组件,找到第一个类型为 BaseInput
的组件并返回。如果没有找到,则创建一个新的 BaseInput
组件并返回。
可以看出,m_InputOverride
是可以覆盖默认的 BaseInput
的,这样就可以通过自定义的 BaseInput
来实现自定义的输入处理。同时类中开放了 inputOverride
属性,用于设置 m_InputOverride
。
此外,通过 eventSystem
属性可以获取当前的 EventSystem
实例。由此可以看出,InputModule
与 EventSystem
是双向关联的。一方面,EventSystem
会在启用时调用 UpdateModules
方法,更新输入模块列表;另一方面,InputModule
会在启用时获取当前的 EventSystem
实例。
1 | [ ] |
接下来是一些虚函数,挑几个重要的分析一下。
-
OnEnable
与OnDisable
方法用于在InputModule
启用和禁用时调用,用于更新输入模块列表。OnEnable
方法会获取当前的EventSystem
实例,并调用UpdateModules
方法更新输入模块列表;OnDisable
方法会调用UpdateModules
方法,将当前的输入模块禁用,并从输入模块列表中移除。 -
Process
方法是BaseInputModule
的核心方法,用于处理当前的输入事件。这里Process
被标记为了abstract
,表示子类必须实现该方法。Process
方法会在每一帧被调用,用于处理当前的输入事件。 -
ActivateModule
、DeactivateModule
、UpdateModule
方法分别用于激活、禁用、更新输入模块。这些方法都是虚方法,可以被子类重写。 -
GetAxisEventData
、GetBaseEventData
方法用于生成轴事件数据和基础事件数据。
1 | protected override void OnEnable() |
接下来是一些工具性的方法,这些方法用于处理输入事件,如射线检测、移动方向的判断等。这些方法大都被标记为 static
,表示这些方法是独立于对象的,可以直接通过类名调用。
-
FindFirstRaycast
方法用于返回第一个有效的射线检测结果。一般而言就是确定用户当前点击的是哪个 UI 元素了。 -
DetermineMoveDirection
方法用于根据输入的移动方向,判断最佳的移动方向。这个方法会根据输入的 x、y 值,判断出最佳的移动方向。 -
FindCommonRoot
方法用于找到两个GameObject
的共同根节点。这个方法会遍历两个GameObject
的父节点,直到找到共同的根节点。值得一提的是,该算法使用的场景有点像 LCA(最近公共祖先)的算法,只不过这里是找到两个
GameObject
的共同根节点。这个算法的时间复杂度是 O(n^2),如果使用 LCA 算法,时间复杂度可以降低到 O(n)。但两个GameObject
不一定在同一棵树上,因此这种暴力的方法也是可以接受的。 -
HandlePointerExitAndEnter
方法用于处理指针的进入和退出事件。这个方法会根据当前的指针数据和新的进入目标,发送相应的进入和退出事件。该方法会调用FindCommonRoot
方法找到两个 GameObject 的共同根节点,然后分别沿着父节点向上发送退出和进入事件。可以看到,事件的处理都是调用ExecuteEvents.Execute
方法来完成,下文中会详细介绍这个方法。
1 | /// <summary> |
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 | /// <summary> |
接下来是一些将触摸输入或鼠标输入转换为对应事件的方法。首先来看 GetTouchPointerEventData
方法。
GetTouchPointerEventData
GetTouchPointerEventData
方法用于将触摸事件转换为 PointerEventData
对象。该方法会根据传入的 input
变量,判断当前是否按下或释放,并将触摸事件的位置、位移、按钮等信息填充到 PointerEventData
对象中。最后,会调用 eventSystem.RaycastAll
及 FindFirstRaycast
方法,获取当前触摸事件对应的射线检测结果,并将其填充到 PointerEventData
对象中。
其中,函数的后两个参数被标记为 out
,表示这两个参数是输出参数,用于返回当前触摸事件是否按下或释放。这种设计方式减少了函数的返回值,提高了代码的可读性。
1 | /// <summary> |
GetMousePointerEventData
接下来是 GetMousePointerEventData
方法。由于鼠标有三个按键,且每个按键都有按下、释放等状态,为了维护鼠标整体的状态,这里定义了许多辅助函数及辅助的类。让我们依次分析:
1 | /// <summary> |
CopyFromTo
方法用于将一个 PointerEventData
对象的属性复制到另一个 PointerEventData
对象中。这个方法在 GetMousePointerEventData
方法中会被频繁调用,用于复制当前鼠标按键的状态。
StateForMouseButton
方法用于根据鼠标按键的 ID,返回当前按键的状态。这个方法会调用 input.GetMouseButtonDown
和 input.GetMouseButtonUp
方法,判断当前按键是否按下或释放。最后,根据按键的状态,返回对应的 PointerEventData.FramePressState
枚举值。
1 | private readonly MouseState m_MouseState = new MouseState(); |
接下来类中维护了一个 MouseState
类型的对象 m_MouseState
,用于存储鼠标的状态。MouseState
类型定义了一个 ButtonState
类型的数组,用于存储鼠标的三个按键的状态,并向外提供了获取及设置这些按键状态的接口。这些类之间的依赖关系如下图所示:
1 | /// <summary> |
分析完了上述的辅助类后,终于可以来看 GetMousePointerEventData
方法了。该方法用于获取当前的鼠标状态。
首先,通过 GetPointerData
方法获取当前鼠标左键对应的事件数据,由上述分析可知,这里使用了缓存的设计。接着,会根据当前的 input
变量,将鼠标的位置、位移、滚轮滚动等信息填充到 PointerEventData
对象中。接下来,会调用 eventSystem.RaycastAll
及 FindFirstRaycast
方法,获取当前鼠标事件对应的射线检测结果,并将其填充到 PointerEventData
对象中。
对于中键和右键,会调用 GetPointerData
方法获取当前鼠标中键和右键对应的事件数据,并调用 CopyFromTo
方法将左键的事件数据复制到中键和右键的事件数据中。最后,会调用 StateForMouseButton
方法,获取当前中键和右键的状态,并调用 m_MouseState.SetButtonState
方法,设置中键和右键的状态。
完成了这些操作后,就可以返回当前的鼠标状态了。这里也使用了缓存的设计,将状态全部更新至 m_MouseState
对象中,供后续使用。
1 | /// <summary> |
接下来有一些对应事件的处理方法,总体来说都是调用与事件类型对应的 ExecuteEvents.Execute
方法。
到这里我们就分析完了 PointerInputModule
类的基本内容,可以看到,PointerInputModule
类将鼠标、触摸等按键的输入转换为了对应的时间数据,并已经定义了相应的处理方法,看上去已经可以处理用户输入了。
然而,PointerInputModule
类依然是一个抽象类,并没有实现 BaseInputModule
中定义的 Process
方法。因此,还需要继承 PointerInputModule
类,并实现 Process
方法,才能真正处理用户的输入事件。
StandaloneInputModule
由类图可知,Unity 中自带两个从 PointerInputModule
派生出的处理输入的类:StandaloneInputModule
与 TouchInputModule
。后者已经被禁用,现在所有的输入均由 StandaloneInputModule
一个类处理。从类的注释中我们也可以得到相同的结论:
1 | [ ] |
由前文对 EvenySystem
的分析可知,该类会逐帧调用 GameObject 上挂载的 InputModule
中的 UpdateModule()
及 Process()
方法以处理用户输入。因此针对 StandaloneInputModule
,我们业主要分析这两部分的实现。
UpdateModule
1 | public override void UpdateModule() |
UpdateModule
方法会在每一帧被调用,用于更新鼠标的位置及其他的类内状态。特别地,如果当前的 EventSystem
失去了焦点,且 ShouldIgnoreEventsOnNoFocus
方法返回 true
,则会调用 ReleaseMouse
方法释放鼠标。
Process
1 | public override void Process() |
Process
方法会在每一帧被调用,用于处理当前的输入事件。该方法会首先调用 SendUpdateEventToSelectedObject
方法,发送更新事件给当前选中的对象。接着,会调用 ProcessTouchEvents
方法处理触摸事件,如果触摸事件处理完毕,则会调用 ProcessMouseEvent
方法处理鼠标事件。最后,如果 eventSystem.sendNavigationEvents
为 true
,则会调用 SendMoveEventToSelectedObject
方法发送移动事件给当前选中的对象,以及调用 SendSubmitEventToSelectedObject
方法发送提交事件给当前选中的对象。
这里多次使用了 usedEvent
变量,用于标识当前的事件是否被处理。如果事件被处理,则会将 usedEvent
置为 true
,表示当前的事件无需进一步处理。
来看一下 ProcessTouchEvents
及 ProcessMouseEvent
方法的实现。
ProcessTouchEvents
1 | private bool ProcessTouchEvents() |
ProcessTouchEvents
方法用于处理触摸事件。该方法会遍历当前的触摸事件,调用 GetTouchPointerEventData
方法将触摸事件转换为 PointerEventData
对象。接着,会调用 ProcessTouchPress
方法处理触摸事件的按下和释放。如果触摸事件未释放,则会调用 ProcessMove
及 ProcessDrag
方法处理触摸事件的移动和拖拽。最后,如果触摸事件已释放,则会调用 RemovePointerData
方法移除对应的事件数据。
有以下几点需要注意:
-
ProcessTouchEvents
整体使用了一个 for 循环来处理所有的触摸事件,这样可以应对诸如多点触控等复杂的触摸事件。 -
ProcessTouchEvents
方法中使用了在PointerInputModule
类中定义的GetTouchPointerEventData
方法,用于将触摸事件转换为PointerEventData
对象。此外,ProcessMove
、ProcessDrag
及RemovePointerData
方法也是在PointerInputModule
类中定义的,用于处理触摸事件的移动、拖拽及释放当前事件。 -
ProcessTouchPress
方法用于处理触摸事件的按下和释放。这个方法会根据触摸事件的按下和释放状态,调用ExecuteEvents.Execute
方法,发送按下和释放事件。
ProcessMouseEvent
1 | protected void ProcessMouseEvent() |
ProcessMouseEvent
方法用于处理鼠标事件。该方法会首先调用 GetMousePointerEventData
方法获取当前的鼠标状态。接着,会调用 ProcessMousePress
方法处理鼠标事件的按下和释放。如果鼠标事件未释放,则会调用 ProcessMove
及 ProcessDrag
方法处理鼠标事件的移动和拖拽。最后,如果鼠标事件有滚动,则会调用 ExecuteEvents.ExecuteHierarchy
方法,发送滚动事件。
有以下几点需要注意:
-
ProcessMouseEvent
方法中使用了在PointerInputModule
类中定义的GetMousePointerEventData
方法,用于获取当前的鼠标状态。此外,该函数也使用了在PointerInputModule
类中定义的ProcessMove
及ProcessDrag
方法,用于处理鼠标事件的移动和拖拽。 -
ProcessMouseEvent
方法中调用了ProcessMousePress
方法,用于处理鼠标事件的按下和释放。这个方法会根据鼠标事件的按下和释放状态,调用ExecuteEvents.Execute
方法,发送按下和释放事件。 -
可以看出除了
ProcessMove
外,ProcessMousePress
及ProcessDrag
都调用了三次,分别处理鼠标的左键、右键和中键。这主要是因为鼠标的移动是一个整体的事件,只需处理一次,而不同按键的按下和释放事件是独立的,因此需要分别处理。
小结
以上我们就分析完了 InputModule
的相关内容。总体而言,InputModule
类主要用于处理用户的输入事件,将用户的输入事件转换为对应的事件数据,并调用 ExecuteEvents.Execute
方法,发送事件给对应的 GameObject
。
其中,BaseInput
负责存放一次输入的相关数据,包括如鼠标位置、滚动、存在状态等;EventData
封装了一次输入的事件数据,负责在 EventSystem
中传递事件信息。InputModule
负责处理用户的输入事件,将用户的输入事件转换为对应的事件数据,并根据输入的类型(如按下、释放、移动等)调用对应的 ExecuteEvents.Execute
方法,发送事件给对应的 GameObject
。
距离我们分析完整个 EventSystem
还有两部分内容:获取当前物体的 Raycast
及负责处理各种事件的 ExecuteEvents
,让我们继续往下看。
Raycasters
在上文的分析中我们可以多次看到 RaycastAll()
以及 FindFirstRaycast()
方法,这些方法都是使用射线检测来获取当前的物体。下面就来分析射线检测模块的实现。
RaycastResult
与 BaseInput
、EventData
类似,射线检测模块也定义了自己的数据结构以用于存储射线检测的结果,这便是 RaycastResult
类。RaycastResult
类的定义如下:
1 | /// <summary> |
其中存放了射线检测的结果,包括检测到的物体、射线检测的距离、检测到的物体的深度等信息。此外,其中还有一个 module
属性用于存放产生这次结果的射线本身。RaycastResult
类还定义了 Clear
方法,用于清空射线检测的结果。
RaycasterManager
RaycasterManager
类用于管理所有的射线检测器。RaycasterManager
类是一个静态类,定义了一个 List<BaseRaycaster>
类型的列表 m_Raycasters
,用于存放所有的射线检测器。
1 | internal static class RaycasterManager |
RaycasterManager
类定义了 AddRaycaster
、GetRaycasters
及 RemoveRaycasters
方法,用于添加、获取及移除射线检测器。
ReflectionMethodsCache
正如 BaseInput
本身并不直接处理用户的输入,而是通过 Unity 的 Input
模块获取结果;Raycasters
本身也并不直接处理射线检测,而是通过 Physics
模块直接获取到各种类型射线检测的函数。这些函数就存放在 ReflectionMethodsCache
类中。
1 | internal class ReflectionMethodsCache |
首先,可以看到类中定义了一系列的委托,用于存放不同种类的射线检测函数。
1 | public delegate bool Raycast3DCallback(Ray r, out RaycastHit hit, float f, int i); |
接着,在 ReflectionMethodsCache
类的构造函数中,通过反射获取了 Physics
及 Physics2D
模块中的射线检测函数,并将其赋值给上述定义的委托。这样,ReflectionMethodsCache
类就可以通过委托调用射线检测函数。
在注释中我们可以看到这样做的目的:
1 | // We call Physics.Raycast and Physics2D.Raycast through reflection to avoid creating a hard dependency from |
这样做主要是为了避免 Raycasters
类直接依赖于 Physics
及 Physics2D
模块,这样可以使得 Raycasters
类不依赖于 Physics
及 Physics2D
模块,从而使得 Raycasters
类可以独立于 Physics
及 Physics2D
模块运行。此外,如果需要其他的方法,需要确保在绑定函数中添加 [RequiredByNativeCode]
,以防止这个函数被裁剪。
ReflectionMethodsCache
是一个单例类,通过 Singleton
属性就可以访问到上述反射出的射线检测函数了。
1 | public static ReflectionMethodsCache Singleton |
BaseRaycaster / PhysicsRaycaster / Physics2DRaycaster
BaseRaycaster
类是所有射线检测器的基类,定义了射线检测器的基本属性及方法。BaseRaycaster
类的定义如下:
1 | /// <summary> |
有以下几点需要注意:
-
BaseRaycaster
类是一个抽象类,定义了抽象方法Raycast
及eventCamera
。Raycast
方法用于射线检测,eventCamera
属性用于获取射线检测的相机。 -
BaseRaycaster
会在启用/禁用时调用RaycasterManager
的AddRaycaster
及RemoveRaycasters
方法,用于将自身添加到/移除出射线检测器列表。
至于 PhysicsRaycaster
及 Physics2DRaycaster
类,这两个类分别用于进行 3D 及 2D 的射线检测。他们继承自 BaseRaycaster
类,并实现了 Raycast
及 eventCamera
方法。2D 与 3D 分别使用了 Physics2D/Physics
中射线穿透获取交点信息的方法。
这里以 PhysicsRaycaster.Raycast()
为例分析一下射线检测的过程:
1 | public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList) |
首先,调用了 ComputeRayAndDistance
方法,用于根据 eventData
中点击的位置得到对应的射线,以及射线在前后裁剪平面的距离(关于裁切平面详见MVP矩阵)。
1 | ray = eventCamera.ScreenPointToRay(eventPosition); |
此后,会调用 ReflectionMethodsCache.Singleton
中相应的射线检测方法,将结果存放在 m_Hits
中。
1 | m_Hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, finalEventMask); |
但 m_Hits
本身为 RaycastHit[]
类型,想要在 EventSystem
中使用,还需要将其转换为 RaycastResult
类型。这里会遍历 m_Hits
,将每一个 RaycastHit
转换为 RaycastResult
,并添加到 resultAppendList
中。
1 | for (int b = 0, bmax = hitCount; b < bmax; ++b) |
这样,PhysicsRaycaster
类就完成了一次射线检测的过程。
小结
至此,我们就分析完了射线检测模块的相关内容。总体而言,射线检测模块主要用于检测当前鼠标位置下的物体,以及获取该次射线检测的相关信息。RaycastResult
类用于存放射线检测的结果,RaycasterManager
类用于管理所有的射线检测器,ReflectionMethodsCache
类用于通过反射获取射线检测函数,BaseRaycaster
类是所有射线检测器的基类,定义了射线检测器的基本属性及方法,其中有一个抽象方法 Raycast
用于射线检测。而 PhysicsRaycaster
及 Physics2DRaycaster
类则分别重写了 Raycast
及 eventCamera
方法,用于进行 3D 及 2D 的射线检测。
ExecuteEvents
ExecuteEvents
是 EventSystem
中的最后一部分内容,它的主要功能为执行各种事件,上文中我们多次看到了 ExecuteEvents.Execute
方法,这个方法就是 ExecuteEvents
类中的一个静态方法。ExecuteEvents
类定义了一系列的静态方法,用于执行 EventSystem
中的事件,将事件发送给对应的 GameObject
。
EventSystemHandler
EventInterfaces.cs
中定义了 EventSystem
中各类事件的接口,最终事件响应的逻辑需要继承这些接口来书写具体的逻辑。
首先,EventInterfaces
中定义了一个公共的接口 IEventSystemHandler
,所有 EventSystem
的事件处理接口均需要继承这个接口。
handler
主要分为三种类型:
-
IPointerXXXHandler
: 处理鼠标点击和触屏事件 -
IDragXXXXHandler
:处理拖拽事件 -
IXXXHandler
:处理其他如选择、取消等事件
1 | public interface IEventSystemHandler |
ExecuteEvents
Handler
ExecuteEvents
类定义了一系列的静态方法,用于执行事件。这些方法主要用于执行 EventSystem
中的事件,将事件发送给对应的 GameObject
。
1 | public delegate void EventFunction<T1>(T1 handler, BaseEventData eventData); |
首先,ExecuteEvents
类定义了一个委托 EventFunction<T1>
,该委托接受一个 T1
类型的参数 handler
和一个 BaseEventData
类型的参数 eventData
,返回 void
。 ValidateEventData
方法用于验证 eventData
是否为 T
类型,如果不是则抛出异常。
接着,ExecuteEvents
类定义了一系列的静态方法,用于执行 EventSystem
中不同类型的事件,如 PointerEnter
、PointerExit
等。这些方法都是通过委托调用 handler
的对应方法,将 eventData
传递给 handler
。
辅助函数
在分析最重要的 Execute
函数之前,先来看一些相关的辅助函数:
1 | private static void GetEventChain(GameObject root, IList<Transform> eventChain) |
GetEventChain
方法用于获取 GameObject
的事件链。该方法会将 root
的所有父节点添加到 eventChain
中。
1 | private static bool ShouldSendToComponent<T>(Component component) where T : IEventSystemHandler |
ShouldSendToComponent
方法用于判断是否应该发送事件给 Component
。该方法会判断 component
是否为 T
类型,以及 component
是否激活。如果满足条件,则返回 true
,否则返回 false
。
1 | /// <summary> |
GetEventList
方法用于获取 GameObject
中类型为 T
的所有事件组件。该方法会遍历 go
的所有组件,如果组件为 T
类型且激活,则将其添加到 results
中。
值得注意的是这里在进行中间的 list 变量处理时,使用了
ListPool
类,这是一个对象池,定义在Core/Utility/ListPool.cs
中,用于减少内存分配。
1 | /// <summary> |
CanHandleEvent
方法用于判断 GameObject
是否能够处理 T
类型的事件。该方法会调用 GetEventList
方法获取 GameObject
中类型为 T
的所有事件组件,如果事件组件的数量不为 0,则返回 true
,否则返回 false
。
1 | /// <summary> |
GetEventHandler
方法用于获取 GameObject
中能够处理 T
类型事件的 第一个 事件处理器。该方法会遍历 root
的所有父节点,如果父节点能够处理 T
类型事件,则返回该父节点。
Execute & ExecuteHierarchy
最后,我们来看一下 Execute
方法:
1 | public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler |
Execute
方法在 InputModule
部分多次被调用,用于执行 EventSystem
中的不同类型的事件。针对一个特定的输入事件类型 T
(如 IPointerEnterHandler
、IPointerExitHandler
等),该方法会获取 target
中所有类型为 T
的事件组件,并调用 functor
方法,将 eventData
传递给这些事件组件。
1 | public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler |
ExecuteHierarchy
方法用于在 root
的事件链中执行 T
类型的事件。该方法会获取 root
的事件链,遍历事件链中的每一个节点,并调用 Execute
方法,将 eventData
传递给这些节点。
小结
至此,我们就分析完了 EventSystem
中的 ExecuteEvents
部分。总体而言,ExecuteEvents
类定义了一系列的静态方法,用于执行 EventSystem
中的事件。这些方法主要用于处理不同类型的 EventSystem
中的事件,并将事件发送给对应的 GameObject
。ExecuteEvents
类中定义了一系列的辅助函数,用于获取 GameObject
的事件链、获取 GameObject
中类型为 T
的所有事件组件、判断 GameObject
是否能够处理 T
类型事件等;同时,ExecuteEvents
类中定义了 Execute
及 ExecuteHierarchy
方法,用于执行 EventSystem
中的不同类型的事件。InputModule
中主要就是通过这些方法来执行 EventSystem
中的事件。
总结
至此,我们就分析完了整个 EventSystem
的实现。EventSystem
是 UGUI 系统中的一个核心组件,用于管理和处理用户输入事件。它是所有 UI 交互的基础,负责检测和分发输入事件,如鼠标点击、触摸、键盘输入等。EventSystem
的主要功能包括:
-
管理所有的输入检测模块(
InputModule
)。EventSystem
使用输入模块(Input Modules)来处理不同类型的输入并逐帧调用 Module 的执行函数Process()
,常见的输入模块包括StandaloneInputModule
(用于鼠标和键盘输入)和TouchInputModule
(用于触摸输入)。 -
调动射线捕捉模块(
Raycasters
),为InputModule
提供结果(具体的触点所穿透的对象信息)。EventSystem
使用射线检测(Raycasting)来确定用户输入的位置和目标。它会发射一条射线,从输入设备(如鼠标或触摸点)的位置出发,检测与之相交的 UI 元素,并将事件传递给 UI 元素。 -
事件监听及处理(
ExecuteEvent
)。EventSystem
会捕获用户的输入事件,生成对应的EventData
并将这些事件分发给相应的 UI 元素。UI 元素可以通过实现特定的接口(如IPointerClickHandler
、IPointerEnterHandler
等)来监听和处理事件。EventSystem
会根据事件类型调用相应的接口方法。