///
/// 对当前 Canvas 上的所有可交互 UI 图形执行射线检测,判断是否被点击或触碰。
///
/// 指针事件的数据(包含鼠标位置、触摸点等)
/// 用于存储命中的 UI 元素结果列表
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 自身设置的显示器
else
displayIndex = 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;
// 初始化阻挡距离为最大值,表示默认没有阻挡物挡住 UI
float 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 模式或没有摄像机,距离为 0
if (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 点击交互相关的关键逻辑部分,并忽略以下非关键内容:
resultAppendList
中供后续事件系统使用var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);
canvasGraphics
是一个 List,里面包含所有可以接收点击事件的 UI 组件(如 Image、Text 等)。raycastTarget == true
color.a > 0
)eventPosition = eventData.position;
eventPosition
是鼠标的屏幕坐标(以像素为单位)///
/// 在屏幕上进行射线检测,并收集所有在该点下方的 Graphic(UI 元素)。
/// 用于事件系统(如点击、拖拽等)判断哪些 UI 元素被交互到。
///
[NonSerialized]
static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
private static void Raycast(
Canvas canvas, // 当前要检测的 Canvas
Camera 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 元素上 |
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 元素,后面的可能不会被检测到 |
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);
}
}
内容 | 说明 |
---|---|
点击检测方式 | 矩形检测(RectTransform 包围盒) |
影响因素 | raycastTarget , alpha , Canvas.renderMode |
点击顺序 | 按照 depth 和 sortingOrder 排序 |
事件分发 | 由 EventSystem 根据命中对象调用 IPointerDownHandler 等接口 |
Unity UGUI (Unity’s User Interface) 事件系统是一个复杂但灵活的机制,用于处理用户交互,如点击、拖动等。
输入检测:
StandaloneInputModule
或TouchInputModule
)监听这些输入。射线投射(Raycast):
GraphicRaycaster
组件对UI进行射线投射。生成指针事件数据:
事件传播:
IPointerEnterHandler
, IPointerExitHandler
, IPointerDownHandler
, IPointerUpHandler
, IClickHandler
等。事件处理:
IPointerClickHandler
接口以处理点击事件),那么该元素就会执行相应的事件处理逻辑。回调和响应:
重复上述过程: