GraphicRaycaster(图形射线检测器)就是专门派驻到每个Canvas(画布)上的“狙击手”。
它的唯一职责就是:当玩家点击屏幕时,算出当前这个 Canvas 里,到底有哪些 UI 元素(Image, Text, RawImage 等)被点中了。
一、 核心属性与配置(狙击手的装备)
在代码的前半部分,定义了这个组件在 Inspector 面板中暴露给开发者的配置项:
ignoreReversedGraphics(忽略反向图形):原理:如果一个 UI 图片在 3D 空间中转了 180 度(背对着相机),你还能点到它吗?如果勾选了这个,代码底层会通过向量点乘(Dot Product)计算 UI 朝向与相机视角的夹角,如果是背对的,直接忽略点击。这在做“UI 翻牌”特效时非常有用。
blockingObjects(阻挡遮罩) 与blockingMask:原理:专为
World Space(世界空间)或Camera Space模式的 Canvas 设计。如果 UI 前面有一堵 3D 的墙,或者 2D 的碰撞体,射线是否应该被墙挡住?如果设为ThreeD或All,它会在检测 UI 之前,先打一根真正的物理射线去探测有没有撞墙。
二、 核心工作流:主Raycast方法步骤拆解
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
这是整个脚本的心脏,每当玩家点击或拖拽时,EventSystem 就会呼叫这个方法。它的执行流程极其严密:
第一步:基础校验与多显示器支持
if (canvas == null) return; var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas); if (canvasGraphics == null || canvasGraphics.Count == 0) return;先看看画布还在不在。然后通过
GraphicRegistry获取这个画布下所有开启了Raycast Target的 UI 元素。接着,代码会计算
displayIndex。如果你的游戏支持多显示器(比如屏幕 1 显示主界面,屏幕 2 显示小地图),它会丢弃那些不在当前点击显示器上的 UI 事件。
第二步:视口裁剪 (Viewport Check)
pos = currentEventCamera.ScreenToViewportPoint(eventPosition); if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f) return;将鼠标屏幕坐标转换为
0 到 1的视口坐标。如果鼠标点在了相机视野范围之外,直接判定为无效,节省后续复杂的数学计算。
第三步:物理阻挡检测 (Physics Blocking) —— 跨界融合的精髓
if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None) { // ... 发射 3D/2D 物理射线 ... if (ReflectionMethodsCache.Singleton.raycast3D(ray, out hit...)) hitDistance = hit.distance; }如果是 Overlay 模式(UI 贴在屏幕最上层),物理阻挡无效。
如果是其他模式,并且开启了阻挡,它会调用底层的物理射线。
精妙之处:它会记录下打中 3D 墙壁的距离,存入
hitDistance。后续算出来的 UI 距离如果大于这个hitDistance,说明 UI 在墙后面,点不到!
第四步:纯粹的 2D UI 几何检测 (调用内部私有 Raycast 方法)
m_RaycastResults.Clear(); Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);这里调用了一个静态内部方法(稍后详细讲),找出了所有鼠标范围内的 UI 元素。
第五步:二次验证与数据打包 (打包发货)
它遍历上一步找到的所有初步符合条件的 UI 元素,进行极其严苛的最终审查:
反向剔除:根据
ignoreReversedGraphics再次确认 UI 是否背对相机。计算真实 3D 距离:利用射线与平面的数学交点公式
distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));算出射线碰触到该 UI 的精确距离。物理墙壁剔除:
if (distance >= hitDistance) continue;—— 墙在距离 5 的地方,UI 在距离 10 的地方,UI 被墙挡住,剔除!生成结果:将通过所有测试的 UI 包装成
RaycastResult结构体,塞入resultAppendList,交给EventSystem去做最终排序。
三、 内部几何检测:私有Raycast方法解析
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)在这个循环里,它对画布上的每一个开启了RaycastTarget的图像进行4 重过滤:
状态过滤:
if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1)如果目标没开射线检测、或者被系统剔除了、或者还没被渲染过,跳过。
矩形包围盒过滤 (Rect Transform Check):
if (!RectTransformUtility.RectangleContainsScreenPoint(...))这是最高效的数学过滤。计算鼠标坐标是否落在 UI 元素的矩形框(RectTransform)范围内。如果连矩形框都没进,直接跳过。
远裁剪面过滤:
if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)如果 UI 跑到了相机视距之外,跳过。
像素级/高级过滤 (Alpha Hit Test):
if (graphic.Raycast(pointerPosition, eventCamera))这一步会调用 UI 元素自身的
ICanvasRaycastFilter。比如:一张不规则的半透明图片,我们设置了 Alpha 阈值(点击透明区域无效),这部分逻辑就是在这个虚方法里执行的。如果点在了透明处,跳过。
最后一步:局部排序
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));把所有选中的 UI,按照它们在 Canvas 内部的depth(底层 Hierarchy 树的渲染顺序)进行降序排列(越后画的排越前面),然后交付给上层。
总结:GraphicRaycaster的设计哲学
不是真物理,而是纯数学:它并没有使用 Unity 的 Collider(碰撞体)来检测点击。对于 UI 来说,碰撞体太重了。它全程使用的是
RectTransform的几何边界计算(包围盒判断)和射线平面交点数学公式,性能极高。层层漏斗,尽早剔除:它不会上来就算复杂的 3D 距离。而是先查屏幕区域 -> 再查包围盒 -> 再查像素 Alpha -> 最后查 3D 遮挡。通过一步步的漏斗,把大部分无效计算在前期就扼杀掉。
完美的解耦:它通过
GraphicRegistry获取目标,而不是自己去GetComponentsInChildren。它把检测结果塞给EventSystem,自己绝不越俎代庖去触发点击事件。分工极其明确。