一、源码/// summary/// 对当前 Canvas 上的所有可交互 UI 图形执行射线检测判断是否被点击或触碰。/// /summary/// param nameeventData指针事件的数据包含鼠标位置、触摸点等/param/// param nameresultAppendList用于存储命中的 UI 元素结果列表/parampublicoverridevoidRaycast(PointerEventDataeventData,ListRaycastResultresultAppendList){// 如果 Canvas 不存在则无法进行任何 UI 检测直接返回if(canvasnull)return;// 获取当前 Canvas 中所有可以参与射线检测的 UI 元素如 Image、Text 等varcanvasGraphicsGraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);// 如果没有图形元素或数量为 0说明没有需要检测的 UI直接返回if(canvasGraphicsnull||canvasGraphics.Count0)return;intdisplayIndex;varcurrentEventCameraeventCamera;// 缓存摄像机引用避免多次调用 Camera.main// 根据 Canvas 渲染模式决定使用哪个显示设备索引多显示器支持if(canvas.renderModeRenderMode.ScreenSpaceOverlay||currentEventCameranull)displayIndexcanvas.targetDisplay;// Overlay 模式下直接使用 Canvas 自身设置的显示器elsedisplayIndexcurrentEventCamera.targetDisplay;// 否则使用摄像机所指向的显示器// 获取鼠标在屏幕上的相对坐标考虑多显示器情况vareventPositionMultipleDisplayUtilities.RelativeMouseAtScaled(eventData.position);// 如果返回值不是 Vector3.zero表示平台支持多显示器系统if(eventPosition!Vector3.zero){inteventDisplayIndex(int)eventPosition.z;// 如果点击发生在其他显示器上则忽略该事件防止跨屏操作if(eventDisplayIndex!displayIndex)return;}else{// 平台不支持多显示器时使用原始事件位置eventPositioneventData.position;#ifUNITY_EDITOR// 在 Unity Editor 中如果 GameView 当前目标显示器与 DisplayIndex 不一致也忽略事件if(Display.activeEditorGameViewTarget!displayIndex)return;// 补充 z 值为当前 GameView 目标显示器编号eventPosition.zDisplay.activeEditorGameViewTarget;#endif}// 将事件位置转换为视口空间坐标范围 [0,1]Vector2pos;if(currentEventCameranull){// 如果是 ScreenSpaceOverlay 模式或没有摄像机使用屏幕分辨率计算比例floatwScreen.width;floathScreen.height;// 如果是其他显示器使用对应显示器的分辨率if(displayIndex0displayIndexDisplay.displays.Length){wDisplay.displays[displayIndex].systemWidth;hDisplay.displays[displayIndex].systemHeight;}posnewVector2(eventPosition.x/w,eventPosition.y/h);}else{// 使用摄像机将屏幕坐标转换为视口坐标poscurrentEventCamera.ScreenToViewportPoint(eventPosition);}// 如果坐标超出摄像机视口范围直接返回无效输入if(pos.x0f||pos.x1f||pos.y0f||pos.y1f)return;// 初始化阻挡距离为最大值表示默认没有阻挡物挡住 UIfloathitDistancefloat.MaxValue;RayraynewRay();// 如果有摄像机生成从摄像机出发到鼠标位置的射线if(currentEventCamera!null)raycurrentEventCamera.ScreenPointToRay(eventPosition);// 如果不是 Overlay 模式并且启用了阻挡对象检测即检查是否有 2D/3D 物体遮挡 UIif(canvas.renderMode!RenderMode.ScreenSpaceOverlayblockingObjects!BlockingObjects.None){// 设置一个默认的射线检测距离100单位用于限制检测深度floatdistanceToClipPlane100.0f;// 如果有摄像机根据摄像机参数动态计算射线长度if(currentEventCamera!null){floatprojectionDirectionray.direction.z;// 避免除以零处理正交投影等情况distanceToClipPlaneMathf.Approximately(0.0f,projectionDirection)?Mathf.Infinity:Mathf.Abs((currentEventCamera.farClipPlane-currentEventCamera.nearClipPlane)/projectionDirection);}#ifPACKAGE_PHYSICS// 如果启用了 3D 阻挡检测if(blockingObjectsBlockingObjects.ThreeD||blockingObjectsBlockingObjects.All){if(ReflectionMethodsCache.Singleton.raycast3D!null){// 执行 3D 射线检测获取所有命中物体varhitsReflectionMethodsCache.Singleton.raycast3DAll(ray,distanceToClipPlane,(int)m_BlockingMask);if(hits.Length0)hitDistancehits[0].distance;// 记录最近的阻挡距离}}#endif#ifPACKAGE_PHYSICS2D// 如果启用了 2D 阻挡检测if(blockingObjectsBlockingObjects.TwoD||blockingObjectsBlockingObjects.All){if(ReflectionMethodsCache.Singleton.raycast2D!null){// 执行 2D 射线检测获取所有命中物体varhitsReflectionMethodsCache.Singleton.getRayIntersectionAll(ray,distanceToClipPlane,(int)m_BlockingMask);if(hits.Length0)hitDistancehits[0].distance;// 记录最近的阻挡距离}}#endif}// 清空临时结果列表准备存储本次射线检测的结果m_RaycastResults.Clear();// 执行对 UI 元素的实际射线检测Raycast(canvas,currentEventCamera,eventPosition,canvasGraphics,m_RaycastResults);inttotalCountm_RaycastResults.Count;// 遍历所有命中候选对象筛选出最终有效的 UI 结果for(varindex0;indextotalCount;index){vargom_RaycastResults[index].gameObject;boolappendGraphictrue;// 如果启用了反向剔除ignoreReversedGraphics检查 UI 是否朝向摄像机if(ignoreReversedGraphics){if(currentEventCameranull){// 没有摄像机时默认 UI 是正向的vardirgo.transform.rotation*Vector3.forward;appendGraphicVector3.Dot(Vector3.forward,dir)0;}else{// 有摄像机时比较 UI 正面和摄像机方向varcameraForwardcurrentEventCamera.transform.rotation*Vector3.forward*currentEventCamera.nearClipPlane;appendGraphicVector3.Dot(go.transform.position-currentEventCamera.transform.position-cameraForward,go.transform.forward)0;}}// 如果需要加入结果if(appendGraphic){floatdistance0;Transformtransgo.transform;Vector3transForwardtrans.forward;// 如果是 Overlay 模式或没有摄像机距离为 0if(currentEventCameranull||canvas.renderModeRenderMode.ScreenSpaceOverlay){distance0;}else{// 使用几何算法计算射线与 UI 平面的交点距离distance(Vector3.Dot(transForward,trans.position-ray.origin)/Vector3.Dot(transForward,ray.direction));// 如果物体在摄像机后方跳过if(distance0)continue;}// 如果 UI 被 3D/2D 物体挡住跳过if(distancehitDistance)continue;// 构建最终的 RaycastResult 并添加进结果列表varcastResultnewRaycastResult{gameObjectgo,modulethis,distancedistance,screenPositioneventPosition,displayIndexdisplayIndex,indexresultAppendList.Count,depthm_RaycastResults[index].depth,sortingLayercanvas.sortingLayerID,sortingOrdercanvas.sortingOrder,worldPositionray.originray.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 元素varcanvasGraphicsGraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);canvasGraphics是一个 List里面包含所有可以接收点击事件的 UI 组件如 Image、Text 等。这些组件必须满足两个条件raycastTarget true不透明度大于 0color.a 02. 获取鼠标位置简化为屏幕坐标eventPositioneventData.position;eventPosition是鼠标的屏幕坐标以像素为单位3. 执行实际的 Raycast 检测/// summary/// 在屏幕上进行射线检测并收集所有在该点下方的 GraphicUI 元素。/// 用于事件系统如点击、拖拽等判断哪些 UI 元素被交互到。/// /summary[NonSerialized]staticreadonlyListGraphics_SortedGraphicsnewListGraphic();privatestaticvoidRaycast(Canvascanvas,// 当前要检测的 CanvasCameraeventCamera,// 拍摄这个 Canvas 的摄像机可以是 UICamera 或世界摄像机Vector2pointerPosition,// 鼠标或触控点在屏幕上的坐标IListGraphicfoundGraphics,// 所有在这个 Canvas 上注册的可交互 Graphic 列表ListGraphicresults// 最终筛选出的、在该点下的 Graphic 结果列表){// 获取当前需要检测的 UI 元素总数inttotalCountfoundGraphics.Count;// 遍历所有在这个 Canvas 上注册的 Graphic 元素for(inti0;itotalCount;i){GraphicgraphicfoundGraphics[i];// 如果// - 不允许射线检测// - 已经被裁剪未显示// - depth -1表示尚未被 Canvas 渲染系统处理过即还未绘制// 就跳过这个元素if(!graphic.raycastTarget||graphic.canvasRenderer.cull||graphic.depth-1)continue;// 检查指针位置是否在该 UI 元素的矩形区域内考虑 paddingif(!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform,pointerPosition,eventCamera,graphic.raycastPadding))continue;// 如果摄像机不为 null并且该 UI 元素的位置在摄像机远裁剪面之外则跳过if(eventCamera!nulleventCamera.WorldToScreenPoint(graphic.rectTransform.position).zeventCamera.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));// 把最终排序后的结果添加到输出列表中totalCounts_SortedGraphics.Count;for(inti0;itotalCount;i)results.Add(s_SortedGraphics[i]);// 清空临时列表以便下次使用s_SortedGraphics.Clear();}术语解释raycastTarget控制该 UI 元素是否参与射线检测是否能接收点击事件canvasRenderer.cull表示该 UI 元素是否被裁剪不在可视区域不会渲染depthUI 元素在 Canvas 下的层级深度决定谁在最上层raycastPadding可选的额外检测范围扩展用于提高命中精度s_SortedGraphics临时缓存满足条件的 UI 元素并根据深度排序RectangleContainsScreenPoint判断鼠标是否点击在一个 UI 元素上4. 对结果进行筛选和排序for(varindex0;indextotalCount;index){vargom_RaycastResults[index].gameObject;// 如果启用了 ignoreReversedGraphics排除背对摄像机的物体这里省略// 检查深度distance如果比阻挡层近才加入结果if(distancehitDistance)continue;// 构造最终的 RaycastResult 并添加进列表varcastResultnewRaycastResult{gameObjectgo,modulethis,distancedistance,screenPositioneventPosition,displayIndexdisplayIndex,indexresultAppendList.Count,depthm_RaycastResults[index].depth,sortingLayercanvas.sortingLayerID,sortingOrdercanvas.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()方法来自定义点击范围比如圆形、多边形等publicclassCircleGraphic:Image{publicoverrideboolRaycast(Vector2sp,CameraeventCamera){// 自定义圆形点击检测returnRectTransformUtility.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状态、播放动画、修改数据模型等。重复上述过程随着用户持续与界面互动上述过程不断重复以实时响应新的输入。