首选需要理解 EventSystem 中的代码结构,EventSystem 目录下包含4个子文件夹,分别是 EventData、InputModules,Raycasters 和 UIElements,UIElements 下是 UI Toolkit 相关代码,这里不做研究,主要关注其他三部分内容
EventData
EventData存储事件信息,PointerEventData存储点击,拖动等信息,AxisEventData存储轴事件数据,如手柄摇杆
InputModules
BaseInputModule(抽象类)引发事件并将其发送给 GameObjects 进行处理
Input是输入系统接口,可以获取输入信息如鼠标位置,但是这部分代码没有开源,而 BaseInput 是对 Input 类的封装
BaseInputModule 有个 BaseInput 类型的变量 m_DefaultInput,子类通过它获取一些输入信息
PointerInputModule(抽象类)用于处理点击类型的输入
StandaloneInputModule 是PC、Mac&Linux上的具体实现,而 TouchInputModule 是IOS、Android等移动平台上的具体实现
EventSystem 上面挂着 StandaloneInputModule 组件,并且运行时会添加 BaseInput 脚本
Raycasters
RaycasterManager 中有个List管理所有的Raycaster
BaseRaycaster(抽象类)负责对场景元素进行射线检测以确定光标是否在它们上面,sortOrderPriority和renderOrderPriority这两个字段用来对结果进行排序,在OnEnable中注册到RaycasterManager,在OnDisable移除,Raycast是抽象方法,在子类中实现
PhysicsRaycaster 是对3D组件进行射线检测,结果根据距离进行排序
Physics2DRaycaster 是对2D组件进行射线检测
GraphicRaycaster 对Graphic元素进行射线检测,所有UI元素(Image,RawImage等)的基类都是Graphic,这里最重要的是2个Raycast重载函数,这里略去不重要的代码
public class GraphicRaycaster : BaseRaycaster
{
[NonSerialized] private List<Graphic> m_RaycastResults = new List<Graphic>();
/// <summary>
/// 对Canvas相关的Graphic元素进行射线检测,结果存入resultAppendList
/// </summary>
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
if (canvas == null)
return;
// 获取Canvas上所有可点击,即raycastTarget = true的Graphic元素
var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);
if (canvasGraphics == null || canvasGraphics.Count == 0)
return;
int displayIndex;
// Canvas为ScreenSpaceOverlay模式下默认为null
var currentEventCamera = eventCamera;
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
displayIndex = canvas.targetDisplay;
else
displayIndex = currentEventCamera.targetDisplay;
// 获取鼠标位置
var eventPosition = Display.RelativeMouseAt(eventData.position);
if (eventPosition != Vector3.zero)
{
int eventDisplayIndex = (int)eventPosition.z;
if (eventDisplayIndex != displayIndex)
return;
}
else
{
eventPosition = eventData.position;
}
// 把eventPosition转换到相机空间
Vector2 pos;
if (currentEventCamera == null)
{
float w = Screen.width;
float h = Screen.height;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
w = Display.displays[displayIndex].systemWidth;
h = Display.displays[displayIndex].systemHeight;
}
pos = new Vector2(eventPosition.x / w, eventPosition.y / h);
}
else
pos = currentEventCamera.ScreenToViewportPoint(eventPosition);
// 不在相机视口内
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
return;
float hitDistance = float.MaxValue;
Ray ray = new Ray();
// currentEventCamera不为空,摄像机发射射线
if (currentEventCamera != null)
ray = currentEventCamera.ScreenPointToRay(eventPosition);
if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
{
// 这里主要检测2D/3D物体对UI的遮挡的,略
}
m_RaycastResults.Clear();
Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
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)
{
// 点乘判断元素是否面向前方
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
}
else
{
// 与摄像头的前方进行比较
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
{
distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));
// 是否在相机后面
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);
}
}
}
/// <summary>
/// 重载函数,收集点击位置处的Graphic元素
/// </summary>
[NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
{
// foundGraphics是Canvas上所有可点击的Graphic元素
int totalCount = foundGraphics.Count;
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];
// -1表示Canvas尚未对其进行处理,也就是说它实际上并未绘制
if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1)
continue;
// 检测点击位置是否在RectTransform形成的矩形内
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();
}
}
跳转到 graphic.Raycast 方法查看后续的检测,这里获取 graphic 上的所有组件转换为接口 ICanvasRaycastFilter,然后调用接口的 IsRaycastLocationValid 方法进行最后的检测
public abstract class Graphic: UIBehaviour,ICanvasElement
{
/// <summary>
/// 确定点击的位置是否合法,sp是Screen point,返回True表示合法
/// </summary>
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;
}
}
Image、Mask以及CanvasGroup都实现了ICanvasRaycastFilter接口,查看Image的具体实现
public class Image : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
{
/// <summary>
/// 计算位置是否为有效的命中位置,根据图片的Alpha值是否大于阈值
/// </summary>
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
if (alphaHitTestMinimumThreshold <= 0)
return true;
if (alphaHitTestMinimumThreshold > 1)
return false;
if (activeSprite == null)
return true;
Vector2 local;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local))
return false;
Rect rect = GetPixelAdjustedRect();
// Convert to have lower left corner as reference point.
local.x += rectTransform.pivot.x * rect.width;
local.y += rectTransform.pivot.y * rect.height;
local = MapCoordinate(local, rect);
// Convert local coordinates to texture space.
Rect spriteRect = activeSprite.textureRect;
float x = (spriteRect.x + local.x) / activeSprite.texture.width;
float y = (spriteRect.y + local.y) / activeSprite.texture.height;
try
{
return activeSprite.texture.GetPixelBilinear(x, y).a >= alphaHitTestMinimumThreshold;
}
catch (UnityException e)
{
Debug.LogError("Using alphaHitTestMinimumThreshold greater than 0 on Image whose sprite texture cannot be read. " + e.Message + " Also make sure to disable sprite packing for this sprite.", this);
return true;
}
}
}
注意:所有勾选 RaycastTarget 的UI元素都会进行射线检测,参与一些坐标,排序计算,数量太多会影响效率,因此不需要交互的元素不要勾选 RaycastTarget
EventSystem
EventSystem 处理输入、射线检测和发送事件,在 EventSystem 中有个列表 m_SystemInputModules 保存所有的输入模块,在Update中更新当前输入模块m_CurrentInputModule,并调用CurrentInputModule.Process方法进行输入处理
在输入模块如 PointerInputModule 中会调用EventSystem.RaycastAll,进行射线检测并将结果保存到列表中,然后调用EventSystem.RaycastComparer对结果进行排序
public class EventSystem : UIBehaviour
{
// 所有的输入模块
private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();
// 当前输入模块,PC上就是StandaloneInputModule
private BaseInputModule m_CurrentInputModule;
protected virtual void Update()
{
// 更新m_SystemInputModules中所有的输入模块
TickModules();
// 更新当前输入模块
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;
}
}
// 当前输入模块为空,选择第一个有效的,略
// Process方法中处理输入
if (!changedModule && m_CurrentInputModule != null)
m_CurrentInputModule.Process();
}
/// <summary>
/// 进行射线检测并将结果添加到raycastResults
/// </summary>
public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
{
raycastResults.Clear();
// 获取所有注册的BaseRaycaster对象,如GraphicRaycaster,PhysicsRaycaster,Physics2DRaycaster
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;
// 进行射线检测,并将结果添加到raycastResults
module.Raycast(eventData, raycastResults);
}
// 对结果进行排序,保证UI优先处理
raycastResults.Sort(s_RaycastComparer);
}
private static readonly Comparison<RaycastResult> s_RaycastComparer = RaycastComparer;
/// <summary>
/// 对射线检测的结果进行比较
/// </summary>
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)
{
// 比较camera深度
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);
}
// 比较sortingLayer
if (lhs.sortingLayer != rhs.sortingLayer)
{
var rid = SortingLayer.GetLayerValueFromID(rhs.sortingLayer);
var lid = SortingLayer.GetLayerValueFromID(lhs.sortingLayer);
return rid.CompareTo(lid);
}
// 比较sortingOrder
if (lhs.sortingOrder != rhs.sortingOrder)
return rhs.sortingOrder.CompareTo(lhs.sortingOrder);
// 比较深度
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);
// 比较index
return lhs.index.CompareTo(rhs.index);
}
}
查看 StandaloneInputModule 中鼠标事件相关方法,这里为了简洁只关注点击方法
public class StandaloneInputModule : PointerInputModule
{
public override void Process()
{
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
return;
bool usedEvent = SendUpdateEventToSelectedObject();
// 优先处理触摸事件,如果有鼠标则处理鼠标事件
if (!ProcessTouchEvents() && input.mousePresent)
ProcessMouseEvent();
}
protected void ProcessMouseEvent()
{
ProcessMouseEvent(0);
}
/// <summary>
/// 处理所有的鼠标事件
/// </summary>
protected void ProcessMouseEvent(int id)
{
var mouseData = GetMousePointerEventData(id);
var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;
m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;
// 优先处理鼠标左键
ProcessMousePress(leftButtonData);
// 处理右键和中键,按下和拖动事件,略。。。
}
/// <summary>
/// 计算和处理任何鼠标按钮的状态变化
/// </summary>
protected void ProcessMousePress(MouseButtonEventData data)
{
var pointerEvent = data.buttonData;
var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
// 按下
if (data.PressedThisFrame())
{
// 给pointerEvent填充数据,略
}
// 抬起
if (data.ReleasedThisFrame())
{
ReleaseMouse(pointerEvent, currentOverGo);
}
}
private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
{
// 触发抬起事件
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick)
{
// 触发鼠标点击事件
ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
}
// 处理拖动相关事件,略
}
}
查看 ExecuteEvents 的具体实现,简单说就是将组件转为IEventSystemHandler,然后执行委托方法
public static class ExecuteEvents
{
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
var internalHandlers = ListPool<IEventSystemHandler>.Get();
GetEventList<T>(target, internalHandlers);
var internalHandlersCount = internalHandlers.Count;
for (var i = 0; i < internalHandlersCount; i++)
{
T arg;
try
{
// 转换成目标类型,如IPointerClickHandler
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;
ListPool<IEventSystemHandler>.Release(internalHandlers);
return handlerCount > 0;
}
/// <summary>
/// 将组件转换成IEventSystemHandler,并放入列表中
/// </summary>
private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler
{
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;
results.Add(components[i] as IEventSystemHandler);
}
ListPool<Component>.Release(components);
}
}
上述代码中的 IEventSystemHandler 是很多其他接口的父接口,Button实现了IPointerClickHandler
所以点击Button,会通过 IPointerClickHandler 接口调用它的 OnPointerClick 方法
参考
关于UGUI中点击判断的原理探究
[UGUI源码一]6千字带你入门UGUI源码
UGUI的点击事件机制