当我们在Unity中创建一个Canvas时,编辑器会默认给我们创建一个EventSystem,其上有两个组件EventSystem 和 StandaloneInputModule如下图
那EventSystem到底是用来干什么的呢?我们找到UGUI底层的源码来一探究竟!UGUI源码
EventSystem在UGUI源码中属于事件逻辑处理模块。所有UI事件都是通过EventSystem类中通过轮询检测到并作相应事件执行,通过调用输入事件检测模块和检测碰撞模块来形成自己的主逻辑。
简要分析EventSystem类中大致进行的处理操作,由于涉及到源码中各类之间功能的互相调用
所以不能在当前EventSystem章中全部细讲。
那我们现在开始吧!
首先定义了一个列表 m_SystemInputModules用来存储输入事件模块
private List m_SystemInputModules = new List();
关于输入事件模块BaseInputModule 会在以后单独开一章去讲解
这里我们可以先大致了解一下BaseInputModule类是一个抽象基类,提供了一些空接口和基本变量。
其中PointerInputModule继承自BaseInputModule,并且在它基础上扩展了关于点位的输入逻辑,也增加了输入类型和状态。
StandaloneInputModule:适用于主机pc上的一些键盘鼠标输入
TouchInputModule:适用于手机上触摸输入
这两个类均继承自PointerInputModule。
所以 m_SystemInputModules中存储的应该是StandaloneInputModule或者TouchInputModule类列表
接着定义了一个BaseInputModule的对象 m_CurrentInputModule 用于获得当前输入模块
private BaseInputModule m_CurrentInputModule;
继续 定义了一个静态static的列表 m_EventSystems
private static List m_EventSystems = new List();
虽然说是列表,但其实在Unity场景下我们只能有且仅有一个EventSystem,所以下面那段代码就是表示了这个意思:
///
/// Return the current EventSystem.
///
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);
}
}
}
接下来就是一堆属性的get set获取,这部分大家可以自行看下源码
终于到函数了.EventSystem中关键的函数UpdateModules(),用于重新更换m_SystemInputModule的列表信息。
通过遍历m_SystemInputModules 一旦列表中模块不存在或者未处于激活状态就进行移除
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);
}
}
与这个UpdateModules()相类似的函数TickModules(),它则是用于更新列表中对应模块的具体信息,可自行查找源码中StandaloneInputModule中找到具体更新函数,这边暂时不细讲。
private void TickModules()
{
for (var i = 0; i < m_SystemInputModules.Count; i++)
{
if (m_SystemInputModules[i] != null)
m_SystemInputModules[i].UpdateModule();
}
}
EventSystem中Update()则是会一直执行TickModules(),并且遍历m_SystemInputModules 通过逻辑返回找到m_CurrentInputModule 当前输入模块 并执行**Process()**用于执行模块中相应的事件,具体例子可以去StandaloneInputModule中找到Process()函数自行查看
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();
}
还有一个函数大家也不会陌生 IsPointerOverGameObject() 这个函数我们经常用来判断是否点击到UI上。
源码中具体是在PointerInputModule中实现,这里暂时不细讲,主要是根据传入一个指针id(默认是鼠标左键-1)来返回对应的物体信息。
我们在实际运用中经常会遇到UI和场景中的可交互物体发生冲突等原因,这个时候我们就可以利用IsPointerOverGameObject()来进行判断点击到的是UI还是别的
void Update()
{
// 判断是否点击了鼠标左键
if(Input.GetMouseButtonDown(0))
{
// 判断当前鼠标指针是否处于UI物体上
if(EventSystem.current.IsPointerOverGameObject())
{
Debug.Log("点击到了UI上");
}
}
}
剩余一些函数SetSelectedGameObject():用于设置当前物体被选中,并且发送OnDeselect事件给之前那个被选中的物体,发送OnSelect事件给当前物体。
该函数通过静态对象EventSystem.current 被其他脚本如:InputField、Selectable所调用
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;
}
这里的m_SelectionGuard默认是false的,并且查看了一下它的引用也都在这个函数中了。
所以正常思路来说第一句if语句是走不进去的。
所以我想的是如果设计到多线程,该函数在被引用的时候,由于线程不安全导致了一个函数执行到m_SelctionGuard = true。
而另一个函数刚刚开始执行第一句if语句,判断为true,进入该语句块中。
不知道这样考虑的对不对,希望看过源码的各路神仙可以帮忙解答一下。
RaycastAll() 这个函数我个人理解就是更新当前射线检测的结果,并对射线碰撞到的所有物体进行由近至远的排序。这个函数在PointerInputModule中被调用,用来获取当前最先被射线检测到物体(即最近的)
public void RaycastAll(PointerEventData eventData, List 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中主要功能都介绍完了。EventSystem类作为事件逻辑处理模块中重要的类,主要是处理输入事件检测模块,对于模块列表信息进行轮询更新,同时获取到当前输入模块后,对该模块进行Process()执行对应事件。其次还有获取射线碰撞检测结果,供PointerInputModule来获取当前最近的碰撞检测对象,以供执行对应事件。
EventSystem类似于一个中转站,和许多模块一起共同协作,定义的函数用来处理主逻辑,许多共有属性多被其他模块或类中进行赋值调用。
最后附上Unity中相关事件
接口 | 事件 | 作用 |
---|---|---|
IPointerEnterHandler | OnPointerEnter | 鼠标进入对象 |
IPointerExitHandler | OnPointerExit | 鼠标离开对象 |
IPointerDownHandler | OnPointerDown | 按下鼠标 |
IPointerClickHandler | OnPointerClick | 点中该对象,在OnPointerUp后发生 |
IPointerUpHandler | OnPointerUp | 松开鼠标 |
接口 | 事件 | 作用 |
---|---|---|
IBeginDragHandler | OnBeginDrag | 开始拖拽,必须同时实现IDragHandler |
IDragHandler | OnDrag | 正在拖拽,每当移动一定距离,就发生一次拖拽事件 |
IEndDragHanlder | OnEndDrag | 结束拖拽,必须同时实现IDragHandler |
InitializePotentialDragHandler | OnInitializePotentialDrag | 可能发生拖拽,必须同时实现IDragHandler,在对象内点击就会发生 |
IDropHandler | OnDrop | 拖拉结束,拖拉开始的地方必须先实现IDragHandler |
接口 | 事件 | 作用 |
---|---|---|
IScrollHandler | OnScroll | 操作鼠标中键的滚轮 |
ISelectHandler | OnSelect | 当EventSystem选中该对象,使用SetSelectedGameObject方法来选中 |
IDeselectHandler | OnDeselect | 不再选中该对象,点击对象外的地方就会变成不选中 |
IUpdateSelectedHandler | OnUpdateSelected | 当对象被选中,则每帧都会发生,对象被选中才会发生 |
ISubmitHandler | OnSubmit | 点击Submit键(默认Enter键),对象被选中才会发生 |
ICancelHandler | OnCancel | 点击Cancel键(默认Esc键),对象被选中才会发生 |
IMoveHandler | OnMove | 点击方向键,对象被选中才会发生 |