Unity UGUI GraphicRaycaster.Raycast详解
一、源码
/// <summary>
/// 对当前 Canvas 上的所有可交互 UI 图形执行射线检测,判断是否被点击或触碰。
/// </summary>
/// <param name="eventData">指针事件的数据(包含鼠标位置、触摸点等)</param>
/// <param name="resultAppendList">用于存储命中的 UI 元素结果列表</param>
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{// 如果 Canvas 不存在,则无法进行任何 UI 检测,直接返回if (canvas == null)return;// 获取当前 Canvas 中所有可以参与射线检测的 UI 元素(如 Image、Text 等)var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);// 如果没有图形元素或数量为 0,说明没有需要检测的 UI,直接返回if (canvasGraphics == null || canvasGraphics.Count == 0)return;int displayIndex;var currentEventCamera = eventCamera; // 缓存摄像机引用(避免多次调用 Camera.main)// 根据 Canvas 渲染模式决定使用哪个显示设备索引(多显示器支持)if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)displayIndex = canvas.targetDisplay; // Overlay 模式下直接使用 Canvas 自身设置的显示器elsedisplayIndex = currentEventCamera.targetDisplay; // 否则使用摄像机所指向的显示器// 获取鼠标在屏幕上的相对坐标(考虑多显示器情况)var eventPosition = MultipleDisplayUtilities.RelativeMouseAtScaled(eventData.position);// 如果返回值不是 Vector3.zero,表示平台支持多显示器系统if (eventPosition != Vector3.zero){int eventDisplayIndex = (int)eventPosition.z;// 如果点击发生在其他显示器上,则忽略该事件,防止跨屏操作if (eventDisplayIndex != displayIndex)return;}else{// 平台不支持多显示器时,使用原始事件位置eventPosition = eventData.position;#if UNITY_EDITOR// 在 Unity Editor 中,如果 GameView 当前目标显示器与 DisplayIndex 不一致,也忽略事件if (Display.activeEditorGameViewTarget != displayIndex)return;// 补充 z 值为当前 GameView 目标显示器编号eventPosition.z = Display.activeEditorGameViewTarget;
#endif}// 将事件位置转换为视口空间坐标(范围 [0,1])Vector2 pos;if (currentEventCamera == null){// 如果是 ScreenSpaceOverlay 模式或没有摄像机,使用屏幕分辨率计算比例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;// 初始化阻挡距离为最大值,表示默认没有阻挡物挡住 UIfloat hitDistance = float.MaxValue;Ray ray = new Ray();// 如果有摄像机,生成从摄像机出发到鼠标位置的射线if (currentEventCamera != null)ray = currentEventCamera.ScreenPointToRay(eventPosition);// 如果不是 Overlay 模式,并且启用了阻挡对象检测(即检查是否有 2D/3D 物体遮挡 UI)if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None){// 设置一个默认的射线检测距离(100单位),用于限制检测深度float distanceToClipPlane = 100.0f;// 如果有摄像机,根据摄像机参数动态计算射线长度if (currentEventCamera != null){float projectionDirection = ray.direction.z;// 避免除以零,处理正交投影等情况distanceToClipPlane = Mathf.Approximately(0.0f, projectionDirection)? Mathf.Infinity: Mathf.Abs((currentEventCamera.farClipPlane - currentEventCamera.nearClipPlane) / projectionDirection);}#if PACKAGE_PHYSICS// 如果启用了 3D 阻挡检测if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All){if (ReflectionMethodsCache.Singleton.raycast3D != null){// 执行 3D 射线检测,获取所有命中物体var hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, (int)m_BlockingMask);if (hits.Length > 0)hitDistance = hits[0].distance; // 记录最近的阻挡距离}}
#endif#if PACKAGE_PHYSICS2D// 如果启用了 2D 阻挡检测if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All){if (ReflectionMethodsCache.Singleton.raycast2D != null){// 执行 2D 射线检测,获取所有命中物体var hits = ReflectionMethodsCache.Singleton.getRayIntersectionAll(ray, distanceToClipPlane, (int)m_BlockingMask);if (hits.Length > 0)hitDistance = hits[0].distance; // 记录最近的阻挡距离}}
#endif}// 清空临时结果列表,准备存储本次射线检测的结果m_RaycastResults.Clear();// 执行对 UI 元素的实际射线检测Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);int totalCount = m_RaycastResults.Count;// 遍历所有命中候选对象,筛选出最终有效的 UI 结果for (var index = 0; index < totalCount; index++){var go = m_RaycastResults[index].gameObject;bool appendGraphic = true;// 如果启用了反向剔除(ignoreReversedGraphics),检查 UI 是否朝向摄像机if (ignoreReversedGraphics){if (currentEventCamera == null){// 没有摄像机时,默认 UI 是正向的var dir = go.transform.rotation * Vector3.forward;appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;}else{// 有摄像机时,比较 UI 正面和摄像机方向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;// 如果是 Overlay 模式或没有摄像机,距离为 0if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay){distance = 0;}else{// 使用几何算法计算射线与 UI 平面的交点距离distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));// 如果物体在摄像机后方,跳过if (distance < 0)continue;}// 如果 UI 被 3D/2D 物体挡住,跳过if (distance >= hitDistance)continue;// 构建最终的 RaycastResult 并添加进结果列表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);}}
}
这段代码是 Unity UGUI 中 GraphicRaycaster
的核心方法之一:Raycast()
,用于 在 2D/3D 场景中检测鼠标点击事件是否命中 UI 元素。
二、 目标
现在只关注 与 2D 点击交互相关的关键逻辑部分,并忽略以下非关键内容:
- 多显示器支持
- 3D 射线检测(blockingObjects)
- 摄像机视口转换
- 反向面剔除(ignoreReversedGraphics)
三、 整体流程简述
- 获取当前 Canvas 上所有可交互的 UI 图形(Graphic)
- 获取鼠标屏幕坐标
- 遍历这些图形,判断是否被鼠标“点中”
- 如果命中,就添加到
resultAppendList
中供后续事件系统使用
四、精简后关键代码解析
1. 获取当前 Canvas 上所有可被射线检测的 UI 元素
var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);
canvasGraphics
是一个 List,里面包含所有可以接收点击事件的 UI 组件(如 Image、Text 等)。- 这些组件必须满足两个条件:
raycastTarget == true
- 不透明度大于 0(
color.a > 0
)
2. 获取鼠标位置(简化为屏幕坐标)
eventPosition = eventData.position;
eventPosition
是鼠标的屏幕坐标(以像素为单位)
3. 执行实际的 Raycast 检测
/// <summary>
/// 在屏幕上进行射线检测,并收集所有在该点下方的 Graphic(UI 元素)。
/// 用于事件系统(如点击、拖拽等)判断哪些 UI 元素被交互到。
/// </summary>
[NonSerialized]
static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();private static void Raycast(Canvas canvas, // 当前要检测的 CanvasCamera eventCamera, // 拍摄这个 Canvas 的摄像机(可以是 UICamera 或世界摄像机)Vector2 pointerPosition, // 鼠标或触控点在屏幕上的坐标IList<Graphic> foundGraphics, // 所有在这个 Canvas 上注册的可交互 Graphic 列表List<Graphic> results // 最终筛选出的、在该点下的 Graphic 结果列表
)
{// 获取当前需要检测的 UI 元素总数int totalCount = foundGraphics.Count;// 遍历所有在这个 Canvas 上注册的 Graphic 元素for (int i = 0; i < totalCount; ++i){Graphic graphic = foundGraphics[i];// 如果:// - 不允许射线检测;// - 已经被裁剪(未显示);// - depth == -1(表示尚未被 Canvas 渲染系统处理过,即还未绘制)// 就跳过这个元素if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1)continue;// 检查指针位置是否在该 UI 元素的矩形区域内(考虑 padding)if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform,pointerPosition,eventCamera,graphic.raycastPadding))continue;// 如果摄像机不为 null,并且该 UI 元素的位置在摄像机远裁剪面之外,则跳过if (eventCamera != null &&eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)continue;// 进一步使用自定义的 Raycast 方法(例如 Image、Text 等子类可能重写)做更精确的检测if (graphic.Raycast(pointerPosition, eventCamera)){// 符合条件的 UI 元素加入临时结果列表s_SortedGraphics.Add(graphic);}}// 对符合条件的 UI 元素按深度(depth)从高到低排序:// - 深度越大,越靠上(覆盖在上面),优先响应事件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();
}
术语 | 解释 |
---|---|
raycastTarget | 控制该 UI 元素是否参与射线检测(是否能接收点击事件) |
canvasRenderer.cull | 表示该 UI 元素是否被裁剪(不在可视区域,不会渲染) |
depth | UI 元素在 Canvas 下的层级深度,决定谁在最上层 |
raycastPadding | 可选的额外检测范围扩展,用于提高命中精度 |
s_SortedGraphics | 临时缓存满足条件的 UI 元素,并根据深度排序 |
RectangleContainsScreenPoint | 判断鼠标是否点击在一个 UI 元素上 |
4. 对结果进行筛选和排序
for (var index = 0; index < totalCount; index++)
{var go = m_RaycastResults[index].gameObject;// 如果启用了 ignoreReversedGraphics,排除背对摄像机的物体(这里省略)// 检查深度(distance),如果比阻挡层近才加入结果if (distance >= hitDistance)continue;// 构造最终的 RaycastResult 并添加进列表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};resultAppendList.Add(castResult);
}
五、 关键逻辑总结
步骤 | 描述 |
---|---|
1. 获取 UI 列表 | 从 GraphicRegistry 获取当前 Canvas 上所有可点击的 UI 元素 |
2. 获取鼠标位置 | 通过 eventData.position 得到鼠标在屏幕上的坐标 |
3. 遍历 UI 元素 | 对每个 UI 元素执行点击检测(矩形区域 + alpha 值) |
4. 排序并返回结果 | 按照深度(depth)、sortingLayer 排序后,返回命中的 UI 元素 |
六、哪些元素会被点击
只有满足以下条件的 UI 元素才会参与点击检测:
条件 | 说明 |
---|---|
raycastTarget == true | 在 Inspector 中勾选了 “Raycast Target” |
color.a > 0 | 透明度不为 0,否则不会响应点击 |
CanvasRenderer 存在 | UI 元素必须有 CanvasRenderer 组件 |
未被遮挡 | 如果前面有更靠前的 UI 元素,后面的可能不会被检测到 |
示例:如何让某个 UI 元素不能被点击
- 把它的
Image.raycastTarget = false
- 或者设置
color.a = 0
(完全透明) - 或者移除
CanvasRenderer
组件(但这样也不会渲染)
七、如何扩展自定义点击行为
可以继承 UI.Graphic
并重写 Raycast()
方法来自定义点击范围(比如圆形、多边形等):
public class CircleGraphic : Image
{public override bool Raycast(Vector2 sp, Camera eventCamera){// 自定义圆形点击检测return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera) && IsInCircle(sp, rectTransform.rect.center, rectTransform.rect.width / 2f);}
}
八、 总结:
2D UI 点击机制关键点
内容 | 说明 |
---|---|
点击检测方式 | 矩形检测(RectTransform 包围盒) |
影响因素 | raycastTarget , alpha , Canvas.renderMode |
点击顺序 | 按照 depth 和 sortingOrder 排序 |
事件分发 | 由 EventSystem 根据命中对象调用 IPointerDownHandler 等接口 |
Unity UGUI (Unity’s User Interface) 事件系统是一个复杂但灵活的机制,用于处理用户交互,如点击、拖动等。
UGUI 事件系统工作流程
-
输入检测:
- Unity首先从输入设备(鼠标、触摸屏、键盘等)接收输入。
- 输入模块(例如
StandaloneInputModule
或TouchInputModule
)监听这些输入。
-
射线投射(Raycast):
- 当接收到输入后,事件系统会通过当前激活的
GraphicRaycaster
组件对UI进行射线投射。 - 这个过程确定了哪个UI元素位于用户的输入位置(比如鼠标点击的位置)。
- 当接收到输入后,事件系统会通过当前激活的
-
生成指针事件数据:
- 根据射线投射的结果,创建相应的指针事件数据(PointerEventData),包括位置信息、点击状态等。
-
事件传播:
- 事件系统根据指针事件数据触发相应的事件。这包括但不限于以下几种类型的事件:
IPointerEnterHandler
,IPointerExitHandler
,IPointerDownHandler
,IPointerUpHandler
,IClickHandler
等。 - 事件按照一定的顺序在UI层次结构中传播,通常是从根到叶子节点(即从父级到子级)。
- 事件系统根据指针事件数据触发相应的事件。这包括但不限于以下几种类型的事件:
-
事件处理:
- 如果某个UI元素实现了对应的接口(例如实现了
IPointerClickHandler
接口以处理点击事件),那么该元素就会执行相应的事件处理逻辑。 - 开发者可以通过实现这些接口来为UI元素添加自定义的交互行为。
- 如果某个UI元素实现了对应的接口(例如实现了
-
回调和响应:
- 在事件处理过程中,可能会调用预设的回调函数或者触发其他游戏逻辑。
- 这些回调可以用来更新UI状态、播放动画、修改数据模型等。
-
重复上述过程:
- 随着用户持续与界面互动,上述过程不断重复,以实时响应新的输入。