UGUI源码分析|02 RayCast
射线检测的本质就是,在屏幕上的点击位置发出一条射线,按照一定规则,筛选被射线命中的物体,最后在排序拿到第一个物体,然后进行各种操作
大致涉及一下类:
- Ray: 射线类
- RaycastResult: 投射结果
- RaycasterManager: 投射器管理器
- BaseRaycaster: 射线投射基类
- GraphicRaycaster : BaseRaycaster: 图形投射器
- PhysicsRaycaster : BaseRaycaster: 针对3D物体的投射器, 需要对象上同时存在Camera组件
- Physics2DRaycaster : PhysicsRaycaster: 针对2D物体的投射器, 需要对象上同时存在Camera组件
Ray
射线,创建射线的方式有很多种,UGUI的射线检测肯定是基于UICamera的
一般有两种方法,都是以相机位置为端点发射射线
目标点可以是近平面上的点,比如Input.mousePosition
var ray = Camera.main.ScreenPointToRay(Vector3 screenPos)
也可以是视口坐标(0-1)
var ray = Camera.main.ViewportPointToRay(Vector3 viewPortPos)
RaycastResult
只是一个数据类,里面包含了很多射线检测的具体数据
RaycastManager
一个静态类,没什么逻辑,会管理所有需要射线检测的物体
internal static class RaycasterManager
{
private static readonly List<BaseRaycaster> s_Raycasters = new List<BaseRaycaster>();
public static void AddRaycaster(BaseRaycaster baseRaycaster)
{
if (s_Raycasters.Contains(baseRaycaster))
return;
s_Raycasters.Add(baseRaycaster);
}
public static List<BaseRaycaster> GetRaycasters()
{
return s_Raycasters;
}
public static void RemoveRaycasters(BaseRaycaster baseRaycaster)
{
if (!s_Raycasters.Contains(baseRaycaster))
return;
s_Raycasters.Remove(baseRaycaster);
}
}
其中BaseRaycaster是一个基类,底层是一个Mono,需要射线检测的对象,需要继承实现它,然后在生命周期内(OnEnable/OnDisable),注册/取消注册,到RaycastManager里
BaseRaycaster
是一个抽象类,需要继承实现
public abstract class BaseRaycaster : UIBehaviour
{}
public abstract class UIBehaviour : MonoBehaviour
{}
如上面所说,会在生命周期内注册到Manager里
protected override void OnEnable()
{
base.OnEnable();
RaycasterManager.AddRaycaster(this);
}
protected override void OnDisable()
{
RaycasterManager.RemoveRaycasters(this);
base.OnDisable();
}
另一个关键方法,需要子类自己实现,其中eventData就是UGUI里传递的数据,需要根据数据,自己去填充结果数组
/// <summary>
/// Raycast against the scene.
/// </summary>
/// <param name="eventData">Current event data.</param>
/// <param name="resultAppendList">List of hit Objects.</param>
public abstract void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList);
GraphicRaycaster
[RequireComponent(typeof(Canvas))]
public class GraphicRaycaster : BaseRaycaster
{}
UGUI的射线检测类,需要Canvas,所以一般就直接挂在Canvas上
所以并不是每一个UGUI元素都会被单独射线检测,整体的检测逻辑是依赖Canvas进行的
- ignoreReversedGraphics 忽略图形背面,通过点乘判断方向,如果是从图形背面传过的射线,会被忽略
- BlockingObjects 物体阻挡,如果点击到对应的物体,则射线不会向下传递,这里的物体不是指UGUI,而是指碰撞盒
接下来才是核心,即Canvas如何收集被点击到的物体,重写了Raycast方法
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
// 拿到Canvas下所有可以被射线检测的Child
var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);
int displayIndex;
var currentEventCamera = eventCamera;
// 如果canvas的模式是overlay,那么使用canvas的指定屏幕
// 否则就使用canvas上挂载的相机的置顶屏幕
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
displayIndex = canvas.targetDisplay;
else
displayIndex = currentEventCamera.targetDisplay;
// 计算点击位置在多屏幕里的位置,不考虑这里了
var eventPosition = Display.RelativeMouseAt(eventData.position);
...
// 转换到视口坐标
Vector2 pos;
if (currentEventCamera == null)
{
// 这里只可能是多显示器的情况,忽略
...
}
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();
if (currentEventCamera != null)
ray = currentEventCamera.ScreenPointToRay(eventPosition);
// 这里是ColliderBox物体阻挡,不考虑了
if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
{
...
}
m_RaycastResults.Clear();
// 真正的射线检测,原理就是用Rect框判断的,代码放在下面
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 (appendGraphic)
{
float distance = 0;
Transform trans = go.transform;
Vector3 transForward = trans.forward;
if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
distance = 0;
else
{
// http://geomalgorithms.com/a06-_intersect-2.html
distance = (Vector3.Dot(transForward, trans.position - ray.origin) /
Vector3.Dot(transForward, ray.direction));
// Check to see if the go is behind the camera.
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);
}
}
}
然后是判断是否点击到的具体方法
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
{
int totalCount = foundGraphics.Count;
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];
// 是否可以点击
if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1)
continue;
// 是否在Rect范围内
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera, graphic.raycastPadding))
continue;
// 是否超过相机原裁面
if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
continue;
// graphic的Raycast方法
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方法,大多数UGUI组件都会继承Graphic,这个方法用于判断自身是否真的被点击到
public virtual bool Raycast(Vector2 sp, Camera eventCamera)
{
...
// 从自身开始
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++)
{
// 如果有canvas组件并且override了,那么不会继续往上递归了
var canvas = components[i] as Canvas;
if (canvas != null && canvas.overrideSorting)
continueTraversal = false;
// UGUI会实现这个接口
var filter = components[i] as ICanvasRaycastFilter;
if (filter == null)
continue;
var raycastValid = true;
var group = components[i] as CanvasGroup;
if (group != null)
{
...
}
else
{
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
if (!raycastValid)
{
ListPool<Component>.Release(components);
return false;
}
}
// 会一直向上递归,直到NULL或者Canvas,所以合法性检测是以Canvas为基的
t = continueTraversal ? t.parent : null;
}
ListPool<Component>.Release(components);
return true;
}