Unity 中各种点击、拖拽的交互响应是不可或缺的。在 UGUI 中,使用了 EventSystem 进行管理。
简介
- 一个简单的交互流程,主要有以下步骤:
- 用户点击、滑动、拖拽鼠标(或屏幕)
- 找到响应的对象
- 执行对象对应的方法
- 因此,相对地,就需要知道
- 如何获取玩家的操作
- 如何找到需要响应的对象
- 如何执行对应方法
- Unity 的事件系统,主要由输入模块(InputModule)、射线检测模块(Raycast)、事件执行模块(ExecuteEvents)组成,在事件系统(EventSystem)的管理下进行。
时序图
- EventSystem 的时序图如下
EventSystem 主流程
- EventSystem 继承 UIBehavior ,作为组件进行使用。其定义了几个对象:
- m_SystemInputModules :当前 EventSystem 的所有 InputModule 列表(List
)。 - m_CurrentInputModule :当前 EventSystem 使用的 InputModule 。
- m_EventSystems :所有 EventSystem 列表(static List
)。 - current :当前使用的 EventSystem 。
- m_SystemInputModules :当前 EventSystem 的所有 InputModule 列表(List
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);
}
else if (index < 0)
{
Debug.LogError("Failed setting EventSystem.current to unknown EventSystem " + value);
}
}
}
...
}
- 当 EveneSystem 激活时,则会加入 m_EventSystems 列表中。同样,当 EveneSystem 关闭时,则会从 m_EventSystems 列表中移除自身, 并且清除掉当前的输入模块信息 m_CurrentInputModule 。
public class EventSystem : UIBehaviour
{
...
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 处于激活状态时,由于继承 UIBehavior ,每帧会执行 Update 方法进行更新,主要内容为:
- 对 m_SystemInputModules 列表中的所有 InputModule 执行 UpdateModule 方法。
- 如果当前输入模块 m_CurrentInputModule 为空,或者不为 m_SystemInputModules 列表中首个符合条件的模块,则进行切换。
- 如果此帧没有进行模块切换,则对当前输入模块 m_CurrentInputModule 执行 Process 方法进行处理。
public class EventSystem : UIBehaviour
{
...
protected virtual void Update()
{
if (current != this)
return;
// 对 m_SystemInputModules 列表中的所有 InputModule 执行 UpdateModule 方法。
TickModules();
//如果当前输入模块 m_CurrentInputModule 为空,或者不为 m_SystemInputModules 列表中首个符合条件的模块,则进行切换。
bool changedModule = false;
var systemInputModulesCount = m_SystemInputModules.Count;
for (var i = 0; i < systemInputModulesCount; 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 < systemInputModulesCount; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported())
{
ChangeEventModule(module);
changedModule = true;
break;
}
}
}
// 如果此帧没有进行模块切换,则对当前输入模块 m_CurrentInputModule 执行 Process 方法进行处理。
if (!changedModule && m_CurrentInputModule != null)
m_CurrentInputModule.Process();
...
}
...
}
- m_SystemInputModules 列表是在 EventSystem.UpdateModules 方法中生成的,即 EventSystem 所属 GameObject 上所有激活的 BaseInputModule 对象。
public class EventSystem : UIBehaviour
{
...
public void UpdateModules()
{
GetComponents(m_SystemInputModules);
var systemInputModulesCount = m_SystemInputModules.Count;
for (int i = systemInputModulesCount - 1; i >= 0; i--)
{
if (m_SystemInputModules[i] && m_SystemInputModules[i].IsActive())
continue;
m_SystemInputModules.RemoveAt(i);
}
}
...
}
- 因此,GameObject 上会挂载 EventSystem 组件,同时会挂载其需要的所有 InputModule 组件。
InputModule 输入模块
- 从 EventSystem 中知道,InputModule 组件需要挂载到同一个 GameObject 上,并且只有激活的才会加入到 EventSystem.m_SystemInputModules 列表中。
- 可以看到,BaseInputModule 中,当对象激活时,会获取组件上的 EventSystem,并且调用 EventSystem.UpdateModules 方法,从而实现将自身加入到 EventSystem.m_SystemInputModules 列表中。同样,当对象关闭时,会再次刷新 EventSystem.m_SystemInputModules 列表,将自身移除。
public abstract class BaseInputModule : UIBehaviour
{
...
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();
}
...
}
- EventSystem 更新时,会先触发 BaseInputModule.UpdateModule 方法,默认情况下,会使用 StandaloneInputModule 组件。其中 UpdateModule 方法,主要记录上一帧和当前帧的输入坐标。另外,如果当前没有聚焦,并且处于拖拽状态,则会释放按压状态,结束拖拽。
public class StandaloneInputModule : PointerInputModule
{
...
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;
}
...
}
- EventSystem 还会对当前输入模块 m_CurrentInputModule 执行 Process 方法,主要进行:
- 对当前选中的 GameObject ,发送更新事件(UpdateEvent)。
- 处理触碰事件(TouchEvent)。
- 如果没有触碰事件,则处理鼠标输入事件(MouseEvent)。
- 对当前选中的 GameObject,发送移动事件(MoveEvent)。
- …
public class StandaloneInputModule : PointerInputModule
{
...
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();
}
}
...
}
- 其中,主要关注的是 TouchEvent 和 MouseEvent。TouchEvent 的主要流程为:
- 调用 EventSystem.RaycastAll 方法,获取射线检测到的第一个 GameObject 。
- 根据当前的按压(pressed)状态、释放(released)状态,发送不同的事件(PointerDown、PointerClick、Drag 等)。
- 如果当前没有释放,则进行移动和拖拽处理。
public class StandaloneInputModule : PointerInputModule
{
...
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;
}
...
}
public abstract class PointerInputModule : BaseInputModule
{
...
protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
{
...
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;
}
...
}
- MouseEvent 和 TouchEvent类似,同样是调用 EventSystem.RaycastAll 方法,获取射线检测到的第一个 GameObject ,对其发送各种事件。
public class StandaloneInputModule : PointerInputModule
{
...
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);
}
}
...
}
public abstract class PointerInputModule : BaseInputModule
{
...
protected virtual MouseState GetMousePointerEventData(int id)
{
...
eventSystem.RaycastAll(leftData, m_RaycastResultCache);
var raycast = FindFirstRaycast(m_RaycastResultCache);
leftData.pointerCurrentRaycast = raycast;
m_RaycastResultCache.Clear();
...
return m_MouseState;
}
...
}
Raycast 射线检测
- EventSystem.RaycastAll 方法,对 RaycasterManager 中 s_Raycasters 列表的激活对象,执行 BaseRaycaster.Raycast 方法,来查找指向的对象。
public class EventSystem : UIBehaviour
{
...
public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
{
raycastResults.Clear();
var modules = RaycasterManager.GetRaycasters();
var modulesCount = modules.Count;
for (int i = 0; i < modulesCount; ++i)
{
var module = modules[i];
if (module == null || !module.IsActive())
continue;
module.Raycast(eventData, raycastResults);
}
raycastResults.Sort(s_RaycastComparer);
}
...
}
- s_Raycasters 列表的对象,是通过 RaycasterManager.AddRaycaster 方法加入的。BaseRaycaster 对象激活时,OnEnable 方法,将自身注册到 s_Raycasters 列表中,关闭时通过 OnDisable 方法将自身移除。所以 RayCaster 对象,也是以组件的形式挂载到 GameObject 上,根据不同的子类型,重写 Raycast 方法,可以实现不同的射线检测模式。
public abstract class BaseRaycaster : UIBehaviour
{
...
public abstract void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList);
...
protected override void OnEnable()
{
base.OnEnable();
RaycasterManager.AddRaycaster(this);
}
protected override void OnDisable()
{
RaycasterManager.RemoveRaycasters(this);
base.OnDisable();
}
...
}
PhysicsRaycaster
- PhysicRaycaster 的 Raycast 代码如下:
public class PhysicsRaycaster : BaseRaycaster
{
...
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
#if PACKAGE_PHYSICS
Ray ray = new Ray();
int displayIndex = 0;
float distanceToClipPlane = 0;
if (!ComputeRayAndDistance(eventData, ref ray, ref displayIndex, 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 != 0)
{
if (hitCount > 1)
System.Array.Sort(m_Hits, 0, hitCount, RaycastHitComparer.instance);
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,
displayIndex = displayIndex,
index = resultAppendList.Count,
sortingLayer = 0,
sortingOrder = 0
};
resultAppendList.Add(result);
}
}
#endif
}
...
}
- 可以看到,PhysicsRaycaster.Raycast 主要做了:
- 通过反射方式,调用 Physics.RaycastAll 或 Physics.Raycast 方法,进行射线检测。
- 对检测结果进行排序。
- 将结果转成 List
对象返回。
- 射线检测是 Unity 的内部方法,通过发射射线,获取所有穿过的碰撞体对象,而排序则是通过 RaycastHitComparer.Compare 实现的。
public class PhysicsRaycaster : BaseRaycaster
{
...
#if PACKAGE_PHYSICS
private class RaycastHitComparer : IComparer<RaycastHit>
{
public static RaycastHitComparer instance = new RaycastHitComparer();
public int Compare(RaycastHit x, RaycastHit y)
{
return x.distance.CompareTo(y.distance);
}
}
#endif
}
- RaycastHitComparer 只考虑远近距离,即通过比较射线接触的物体的距离,从近到远进行排序。
Physics2DRaycaster
- Physics2DRaycaster 的 Raycast 代码如下:
public class Physics2DRaycaster : PhysicsRaycaster
{
...
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
#if PACKAGE_PHYSICS2D
Ray ray = new Ray();
float distanceToClipPlane = 0;
int displayIndex = 0;
if (!ComputeRayAndDistance(eventData, ref ray, ref displayIndex, ref distanceToClipPlane))
return;
int hitCount = 0;
if (maxRayIntersections == 0)
{
if (ReflectionMethodsCache.Singleton.getRayIntersectionAll == null)
return;
m_Hits = ReflectionMethodsCache.Singleton.getRayIntersectionAll(ray, distanceToClipPlane, finalEventMask);
hitCount = m_Hits.Length;
}
else
{
if (ReflectionMethodsCache.Singleton.getRayIntersectionAllNonAlloc == null)
return;
if (m_LastMaxRayIntersections != m_MaxRayIntersections)
{
m_Hits = new RaycastHit2D[maxRayIntersections];
m_LastMaxRayIntersections = m_MaxRayIntersections;
}
hitCount = ReflectionMethodsCache.Singleton.getRayIntersectionAllNonAlloc(ray, m_Hits, distanceToClipPlane, finalEventMask);
}
if (hitCount != 0)
{
for (int b = 0, bmax = hitCount; b < bmax; ++b)
{
Renderer r2d = null;
// Case 1198442: Check for 2D renderers when filling in RaycastResults
var rendererResult = m_Hits[b].collider.gameObject.GetComponent<Renderer>();
if (rendererResult != null)
{
if (rendererResult is SpriteRenderer)
{
r2d = rendererResult;
}
#if PACKAGE_TILEMAP
if (rendererResult is TilemapRenderer)
{
r2d = rendererResult;
}
#endif
if (rendererResult is SpriteShapeRenderer)
{
r2d = rendererResult;
}
}
var result = new RaycastResult
{
gameObject = m_Hits[b].collider.gameObject,
module = this,
distance = Vector3.Distance(eventCamera.transform.position, m_Hits[b].point),
worldPosition = m_Hits[b].point,
worldNormal = m_Hits[b].normal,
screenPosition = eventData.position,
displayIndex = displayIndex,
index = resultAppendList.Count,
sortingLayer = r2d != null ? r2d.sortingLayerID : 0,
sortingOrder = r2d != null ? r2d.sortingOrder : 0
};
resultAppendList.Add(result);
}
}
#endif
}
...
}
- Physics2DRaycaster.Raycast 和 PhysicsRaycaster.Raycast 相比,少了排序的操作,其主要做了:
- 通过反射调用 Physics2D.GetRayIntersectionAll 或 Physics2D.GetRayIntersectionNonAlloc 进行射线检测。
- 获取射线检测到的对象上的 Renderer(SpriteRenderer、TilemapRenderer、SpriteShapeRenderer)。
- 将结果转成 List
对象返回。
GraphicRaycaster
- GraphicRaycaster 重写的 Raycast 方法,部分代码如下:
public class GraphicRaycaster : BaseRaycaster
{
...
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
if (canvas == null)
return;
// 获取 canvas 下的 graphic 对象
var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);
if (canvasGraphics == null || canvasGraphics.Count == 0)
return;
...
// 获取事件坐标
var eventPosition = Display.RelativeMouseAt(eventData.position);
...
float hitDistance = float.MaxValue;
Ray ray = new Ray();
if (currentEventCamera != null)
ray = currentEventCamera.ScreenPointToRay(eventPosition);
...
m_RaycastResults.Clear();
// 进行 Raycast 检测
Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
// 将结果转到 List<RaycastResult> 列表返回
int totalCount = m_RaycastResults.Count;
for (var index = 0; index < totalCount; index++)
{
var go = m_RaycastResults[index].gameObject;
bool appendGraphic = true;
if (ignoreReversedGraphics)
{
if (currentEventCamera == null)
{
// If we dont have a camera we know that we should always be facing forward
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
}
else
{
// If we have a camera compare the direction against the cameras forward.
var cameraForward = currentEventCamera.transform.rotation * Vector3.forward * currentEventCamera.nearClipPlane;
appendGraphic = Vector3.Dot(go.transform.position - currentEventCamera.transform.position - cameraForward, go.transform.forward) >= 0;
}
}
if (appendGraphic)
{
float distance = 0;
Transform trans = go.transform;
Vector3 transForward = trans.forward;
if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
distance = 0;
else
{
// http://geomalgorithms.com/a06-_intersect-2.html
distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));
// Check to see if the go is behind the camera.
if (distance < 0)
continue;
}
if (distance >= hitDistance)
continue;
var castResult = new RaycastResult
{
gameObject = go,
module = this,
distance = distance,
screenPosition = eventPosition,
displayIndex = displayIndex,
index = resultAppendList.Count,
depth = m_RaycastResults[index].depth,
sortingLayer = canvas.sortingLayerID,
sortingOrder = canvas.sortingOrder,
worldPosition = ray.origin + ray.direction * distance,
worldNormal = -transForward
};
resultAppendList.Add(castResult);
}
}
}
...
}
- 从流程上看,GraphicRaycaster 重写的 Raycast 方法,创建射线后,并没有直接使用射线检测,而是通过调用内部的 Raycast 方法进行检测,再将结果返回。
public class GraphicRaycaster : BaseRaycaster
{
...
[NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
{
// Necessary for the event system
int totalCount = foundGraphics.Count;
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];
// -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1)
continue;
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera, graphic.raycastPadding))
continue;
if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
continue;
if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
}
}
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
totalCount = s_SortedGraphics.Count;
for (int i = 0; i < totalCount; ++i)
results.Add(s_SortedGraphics[i]);
s_SortedGraphics.Clear();
}
}
- GraphicRaycaster 内部的 Raycast 方法,主要步骤:
- 剔除不需要进行检测的情况(如 raycastTarget 关闭,响应点坐标不在 rectTransform 范围内、超过远裁剪平面等)。
- 调用 Graphic.Raycast 方法,判断当前 graphic 是否符合响应坐标,符合则加入 s_SortedGraphics 列表。
- 对 s_SortedGraphics 列表的对象,根据深度从大到小排序。
- 将 s_SortedGraphics 对象转到 results 中并返回。
- Graphic.Raycast 方法的代码如下:
public abstract class Graphic : UIBehaviour, ICanvasElement
{
...
public virtual bool Raycast(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled)
return false;
var t = transform;
var components = ListPool<Component>.Get();
bool ignoreParentGroups = false;
bool continueTraversal = true;
while (t != null)
{
t.GetComponents(components);
for (var i = 0; i < components.Count; i++)
{
var canvas = components[i] as Canvas;
if (canvas != null && canvas.overrideSorting)
continueTraversal = false;
var filter = components[i] as ICanvasRaycastFilter;
if (filter == null)
continue;
var raycastValid = true;
var group = components[i] as CanvasGroup;
if (group != null)
{
if (ignoreParentGroups == false && group.ignoreParentGroups)
{
ignoreParentGroups = true;
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
else if (!ignoreParentGroups)
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
else
{
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
if (!raycastValid)
{
ListPool<Component>.Release(components);
return false;
}
}
t = continueTraversal ? t.parent : null;
}
ListPool<Component>.Release(components);
return true;
}
...
}
- Graphic.Raycast 方法的流程为:
- 获取自身所有 Graphic 组件。
- 对每一个继承 ICanvasRaycastFilter 接口的 Graphic 组件,执行 ICanvasRaycastFilter.IsRaycastLocationValid 方法,检测响应点是否对此组件有效,如果无效则直接返回。
- 逐级向上,直到 Canvas 设置了 overrideSorting ,或者直到根节点,返回有效。
- GraphicRaycaster 内部的 Raycast 方法主要通过 RectTransform 的 Rect 来过滤组件,而 Graphic 的 Raycast 主要是用来确认该组件是否被射线检测命中。
ExecuteEvents 执行事件
- 射线检测模块完成后,得到了 RaycastResult 数据,接下来就要执行相关的事件。继续看 StandaloneInputModule.ProcessTouchPress 方法,部分代码如下:
public class StandaloneInputModule : PointerInputModule
{
...
protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
{
var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
// PointerDown notification
if (pressed)
{
...
var newClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
...
pointerEvent.pointerClick = newClick;
...
}
// PointerUp notification
if (released)
{
...
// see if we mouse up on the same element that we clicked on...
var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
// PointerClick and Drop events
if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick)
{
ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
}
...
pointerEvent.pointerClick = null;
...
}
m_InputPointerEvent = pointerEvent;
}
...
}
- 这里以点击事件为例,射线检测完成后,得到了目标 gameObject ,当按下时,通过调用 ExecuteEvents.GetEventHandler
方法获取点击事件响应的最终 gameObject。当释放时,如果还是同一个 gameObject,则执行 ExecuteEvents.Execute 方法执行点击事件。
public static class ExecuteEvents
{
...
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;
}
/// <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);
var componentsCount = components.Count;
for (var i = 0; i < componentsCount; 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 + ">");
}
/// <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;
}
/// <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;
}
}
- 可以看到,对于点击事件 IPointerClickHandler,对射线检测得到的对象,会获取其挂载的 IPointerClickHandler 类型的激活状态的组件,找到了则返回 gameObject 。如果找不到,则从父节点中继续查找。
public static class ExecuteEvents
{
...
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);
var internalHandlersCount = internalHandlers.Count;
for (var i = 0; i < internalHandlersCount; 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;
}
...
}
- ExecuteEvents.Execute
方法,则根据对应的事件类型,如:点击事件 IPointerClickHandler,通过调用 GetEventList 获取类型为 IPointerClickHandler 的组件,并对所有执行 IPointerClickHandler.OnPointerClick(PointerEventData eventData) 方法,其他事件类型同理,可在 ExecuteEvents 中找到对应实现。
总结
- 通过了解 Unity 的事件系统的运作流程,可以对其有更加清晰的认识,也能更好地使用。
- 对于特殊的需求,也可以通过重写不同模块的方法来自定义,如:
- 重写 InputModule 的 Process 方法, 自定义输入系统处理流程。
- 重写 InputModule 的 GetTouchPointerEventData 方法,自定义触碰事件数据生成方法。
- 重写 Raycast 的 Raycast 方法,自定义射线检测处理规则。
- …