Unity Physics2D 如何区分表面命中与射线起点位于碰撞体内部?
先检查是否命中了 Collider2D,再通过 hit.fraction == 0f 识别射线起点处的命中。对于题述从碰撞体内部开始的情况,Unity 明确规定返回的 RaycastHit2D.fraction 为零,因此无需比较或四舍五入 normal。前提是 Physics2D.queriesStartInColliders 允许返回射线起点所在的碰撞体。Unity Physics2D.Raycast 文档
为什么比较 normal 无法可靠区分
射线从碰撞体内部开始时,没有经过可用于计算表面法线的入口交点,因此 Unity 返回的 normal 是射线方向的反方向。
射线从外部垂直射向表面时,真实表面法线也可能与射线方向完全相反。这两种情况可以有相同的方向关系,所以无论比较向量、使用点积,还是放宽角度容差,都无法仅凭 normal 区分。
如何判断一次命中的类型
下面的代码按上述顺序判断命中类型。调用时传入有效的非零方向和正的有限检测距离:
using UnityEngine;
public static class RaycastClassifier2D
{
public static void CheckHit(
Vector2 origin,
Vector2 direction,
float maxDistance,
LayerMask layerMask)
{
RaycastHit2D hit = Physics2D.Raycast(
origin, direction, maxDistance, layerMask);
if (hit.collider == null)
{
Debug.Log("No hit.");
return;
}
if (hit.fraction == 0f)
{
Debug.Log("Hit at ray origin; skip repeated collision response.");
return;
}
Debug.Log($"Surface hit: {hit.point}, normal: {hit.normal}");
// 在这里处理正常的表面命中。
}
}
这里直接与 0f 比较,是在检查 API 规定的返回值,而不是比较两个独立计算得到的近似浮点数。改用 Mathf.Approximately 或自设阈值,会把部分距离很近的正常命中也归入起点命中,因此需要有明确的业务理由。
查询语义和几何语义也要区分:fraction == 0f 表示命中发生在起点处,不能据此断言该点严格位于几何内部且与边界有正距离。要防止零距离命中反复触发响应,用这个判断更直接。
跳过起点命中后,如何继续检测前方
单结果 Physics2D.Raycast 返回起点命中后,直接忽略该结果不会自动取得前方的其他碰撞体。如果还要继续检测,可以使用 Physics2D.RaycastAll,过滤零比例结果,再选取第一个有效命中。该 API 按距离升序返回结果。Unity Physics2D.RaycastAll 文档
RaycastHit2D[] hits = Physics2D.RaycastAll(
origin, direction, maxDistance, layerMask);
foreach (RaycastHit2D hit in hits)
{
if (hit.collider == null || hit.fraction == 0f)
continue;
// 处理最近的非起点命中。
HandleSurfaceHit(hit);
break;
}
HandleSurfaceHit 代表项目自己的碰撞处理方法。高频调用时,需要考虑 RaycastAll 返回数组的分配开销,可以按所用 Unity 版本选择接收可复用结果容器的查询重载。
什么时候可以关闭内部起点检测
如果整个项目都不需要返回射线起点所在的碰撞体,可以设置:
Physics2D.queriesStartInColliders = false;
这是全局设置,会影响相关 raycast 和 linecast 查询。使用前应确认其他系统是否依赖内部起点检测。该设置控制是否返回这些碰撞体,不能用来取得同一碰撞体的出口交点。Unity queriesStartInColliders 文档
过滤零比例命中只能解决重复响应的识别问题,不会把已经进入碰撞体的起点移回外部。如果后续计算必须依赖外部起点,仍需单独修正位置,不能把过滤结果当作穿透已经消除。
备注:内容仅供参考。
