关于 unity-physics-queries
面向 Unity 开发者的物理查询正确性模式技能,帮助选择合适的查询类型并理解常见 API 陷阱。内容涉及命中排序、非分配缓冲区、LayerMask、触发器及场景专属查询。
主要功能
- 01按需求选择查询类型
- 02处理起点与碰撞体重叠
- 03排序多命中结果
- 04管理 NonAlloc 缓冲区
- 05正确构造 LayerMask
- 06控制触发器与场景查询
适用场景
- 01修正角色接地检测漏判
- 02处理穿透多个物体的射线结果
- 03避免 NonAlloc 查询丢失命中
- 04隔离附加场景中的物理检测
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-physics-queries安装后在 Claude Code 中输入/unity-physics-queries确认已加载。
附带文件references/query-decision-tree.md
详细介绍
物理查询模式——正确性模式
前置技能:
unity-physics(Rigidbody、碰撞体、射线检测 API)、unity-foundations(层、GameObject)
这些模式针对最常见的物理查询错误:使用了错误的查询类型、误解内存分配,以及忽略会导致静默失败的细微默认设置。
模式:选择查询类型
何时:选择要使用哪种物理查询
错误(Claude 默认):
// Always defaulting to Raycast for everything
Physics.Raycast(origin, direction, out hit, maxDistance);
正确——使用决策树:
Need to detect...
|
+-- "Is anything there?" (binary yes/no)
| --> CheckSphere, CheckBox, CheckCapsule (returns bool, cheapest)
|
+-- "What's the nearest thing along a line?"
| --> Raycast (single closest hit along an infinitely thin line)
|
+-- "What's the nearest thing along a volume?"
| --> SphereCast, BoxCast, CapsuleCast (sweep a shape, single closest hit)
|
+-- "Everything along a line?"
| --> RaycastAll / RaycastNonAlloc (all hits, not just closest)
|
+-- "Everything inside an area?"
--> OverlapSphere, OverlapBox, OverlapCapsule (all colliders in region)
注意:Raycast 只返回最近的命中结果。如果需要穿透多个对象,请使用 RaycastAll 或 RaycastNonAlloc。如果需要检测某个区域内的所有对象(例如爆炸半径),应使用 OverlapSphere,而不是 SphereCast。
模式:投射起点位于碰撞体内部
何时:SphereCast/BoxCast/CapsuleCast 的起点与现有碰撞体重叠
错误(Claude 默认):
// Expecting to detect the ground when the sphere starts inside it
if (Physics.SphereCast(feetPosition, radius, Vector3.down, out hit, 0.1f))
{
grounded = true; // May MISS if sphere starts inside the ground collider
}
正确:
// Casts do NOT detect colliders that the shape starts inside of
// Use Overlap for "what am I currently touching?"
grounded = Physics.CheckSphere(feetPosition, radius, groundMask);
// Or use OverlapSphere to get the actual colliders:
Collider[] touching = Physics.OverlapSphere(feetPosition, radius, groundMask);
注意:这适用于所有投射查询(SphereCast、BoxCast、CapsuleCast、Raycast)。如果起点位于碰撞体内部,该碰撞体就会被忽略。这是“我的地面检测不起作用”问题的头号来源。从 MeshCollider 内部开始的射线检测也会漏掉该碰撞体。请使用 Overlap* 或 Check* 检测当前重叠情况。
模式:不保证命中结果的顺序
何时:使用 RaycastAll 或 RaycastNonAlloc 并期望结果已排序
错误(Claude 默认):
RaycastHit[] hits = Physics.RaycastAll(origin, direction, maxDist);
// Assuming hits[0] is the closest
ProcessHit(hits[0]);
正确:
RaycastHit[] hits = Physics.RaycastAll(origin, direction, maxDist);
// Results are NOT sorted by distance -- sort manually
System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
if (hits.Length > 0)
ProcessHit(hits[0]); // Now this is the closest
注意:普通的 Physics.Raycast(单次命中)始终返回最近的命中结果。只有 RaycastAll 和 RaycastNonAlloc 返回的结果未排序。SphereCastAll/SphereCastNonAlloc 等也同样如此。对于 NonAlloc,只对返回数量范围内的结果排序,不要对整个缓冲区排序。
模式:NonAlloc 缓冲区大小和返回数量
何时:使用 RaycastNonAlloc、OverlapSphereNonAlloc 或类似的零分配查询
错误(Claude 默认):
// Buffer of 1 -- silently drops extra results
RaycastHit[] buffer = new RaycastHit[1];
int count = Physics.RaycastNonAlloc(ray, buffer, maxDist);
正确:
// Pre-allocate a reasonably sized buffer as a class field
private readonly RaycastHit[] _hitBuffer = new RaycastHit[16];
void DetectHits()
{
int count = Physics.RaycastNonAlloc(ray, _hitBuffer, maxDist, layerMask);
// ONLY iterate up to count, not buffer.Length
for (int i = 0; i < count; i++)
{
ProcessHit(_hitBuffer[i]);
}
// If count == buffer.Length, you may have missed results
if (count == _hitBuffer.Length)
Debug.LogWarning("Hit buffer full -- may have missed results");
}
注意:NonAlloc 会填充提供的缓冲区,并返回写入的结果数量。如果结果数量超过缓冲区容量,多余结果会被静默丢弃,不会报错。请根据使用场景,将缓冲区大小设为预期结果数量上限。常见大小:地面检测 = 4,爆炸半径 = 32,广域扫描 = 64。
模式:LayerMask 位移与 GetMask
何时:为物理查询构造层遮罩
错误(Claude 默认):
// DOUBLE-SHIFTING: GetMask already returns a bitmask, not a layer index
int mask = 1 << LayerMask.GetMask("Ground"); // WRONG -- shifts a bitmask by a bitmask amount
正确:
// GetMask returns the final bitmask -- use directly
int groundMask = LayerMask.GetMask("Ground");
int multiMask = LayerMask.GetMask("Ground", "Water", "Default");
// NameToLayer returns the layer INDEX -- this one needs the shift
int groundLayer = LayerMask.NameToLayer("Ground"); // Returns e.g. 8
int groundMask2 = 1 << groundLayer; // Correct: 1 << 8 = 256
// Combining with bitwise OR
int combinedMask = (1 << LayerMask.NameToLayer("Ground")) | (1 << LayerMask.NameToLayer("Water"));
// Inverting a mask (everything EXCEPT these layers)
int everythingButGround = ~LayerMask.GetMask("Ground");
注意:LayerMask.GetMask("Ground") = 位掩码(例如 256)。LayerMask.NameToLayer("Ground") = 索引(例如 8)。gameObject.layer = 索引。需要传入掩码时却传入层索引(反之亦然),会静默筛选错误的层,且不会报错。
模式:QueryTriggerInteraction 默认值
何时:射线检测或其他查询意外命中触发器碰撞体
错误(Claude 默认):
// Assuming triggers are ignored by queries
if (Physics.Raycast(origin, direction, out hit, maxDist, layerMask))
{
// hit.collider might be a trigger!
}
正确:
// Explicitly control trigger interaction
if (Physics.Raycast(origin, direction, out hit, maxDist, layerMask, QueryTriggerInteraction.Ignore))
{
// Guaranteed to only hit non-trigger colliders
}
// Or check at the hit level
if (Physics.Raycast(origin, direction, out hit, maxDist, layerMask))
{
if (!hit.collider.isTrigger)
{
// Process only non-trigger hits
}
}
注意:默认值为 QueryTriggerInteraction.UseGlobal,它会读取 Physics.queriesHitTriggers。该全局默认值为true,这意味着查询默认会命中触发器。这会让许多开发者措手不及。如果命中触发器会导致问题(地面检测、视线检测、子弹轨迹检测),请显式设置此值。
模式:SphereCast 半径与距离
何时:使用 SphereCast 时混淆参数含义
错误(Claude 默认):
// Confusing parameters: treating radius as detection range
Physics.SphereCast(origin, detectionRange, direction, out hit);
// This creates a sphere with radius=detectionRange that travels infinitely far
正确:
// radius = SIZE of the sphere being swept
// maxDistance = how FAR the sphere travels
float sphereRadius = 0.5f;
float castDistance = 10f;
if (Physics.SphereCast(origin, sphereRadius, direction, out hit, castDistance, layerMask))
{
// hit.distance = distance the sphere CENTER traveled, not the surface
// hit.point = point on the surface of the OTHER collider (not the sphere)
}
注意:hit.distance 是球体中心在接触前移动的距离,而不是从起点到命中表面的总距离。实际接触表面位于 hit.point。radius=0 的 SphereCast 行为与 Raycast 相同。如果球体很大而投射距离很短,可能会因“起点位于碰撞体内部”问题而漏掉附近对象。
模式:CapsuleCast 点参数
适用场景:设置 CapsuleCast 端点
错误(Claude 默认):
// Using center + full height
Physics.CapsuleCast(center, center + Vector3.up * height, radius, direction, out hit);
正确:
// point1 and point2 are the centers of the two HEMISPHERES (not the full endpoints)
// For a character with height 2.0 and radius 0.5:
float height = 2.0f;
float radius = 0.5f;
Vector3 point1 = center + Vector3.up * (height * 0.5f - radius); // Top hemisphere center
Vector3 point2 = center - Vector3.up * (height * 0.5f - radius); // Bottom hemisphere center
Physics.CapsuleCast(point1, point2, radius, direction, out hit, maxDistance, layerMask);
注意:胶囊体总高度 = |point2 - point1| + 2 * radius。如果 point1 == point2,则会退化为 SphereCast。CapsuleCollider 组件采用不同的定义方式(center + height + radius),因此从 CapsuleCollider 转换时需要使用:point1 = center + up * (height/2 - radius),point2 = center - up * (height/2 - radius)。
模式:背面检测
适用场景:从背面向 MeshCollider 进行射线检测
错误(Claude 默认):
// Assuming raycasts hit both sides of a mesh triangle
if (Physics.Raycast(insidePoint, direction, out hit))
{
// May not hit if ray goes through backface of MeshCollider
}
正确:
// Enable backface hits globally (affects all queries)
Physics.queriesHitBackfaces = true;
// Or design around it:
// Convex MeshColliders are always hit from both sides
// Primitive colliders (Box, Sphere, Capsule) are always hit from both sides
// Only non-convex MeshColliders have single-sided detection by default
注意:默认情况下,Physics.queriesHitBackfaces = false。这只影响非凸 MeshCollider。Box、Sphere、Capsule 和凸 MeshCollider 都能检测来自任意方向的命中。如果需要从非凸网格内部进行射线检测(例如房间内部),可以启用背面查询,或对内部使用凸碰撞体。
模式:特定场景查询
适用场景:使用具有独立物理模拟的叠加场景
错误(Claude 默认):
// Global queries search ALL physics scenes
Collider[] results = Physics.OverlapSphere(center, radius);
正确:
// Get the physics scene for a specific Unity scene
PhysicsScene physScene = gameObject.scene.GetPhysicsScene();
// Query only within that physics scene
RaycastHit hit;
if (physScene.Raycast(origin, direction, out hit, maxDist, layerMask))
{
// Only hits colliders in this physics scene
}
// OverlapSphere with scene scope
Collider[] buffer = new Collider[32];
int count = physScene.OverlapSphere(center, radius, buffer, layerMask);
注意:默认情况下,所有场景共享同一个 Physics.defaultPhysicsScene。只有在明确使用 LocalPhysicsMode.Physics3D 创建场景时,特定场景的物理设置才会起作用。大多数项目都不需要这样做——但对于多人游戏预测、并行模拟或编辑器预览场景,这一点至关重要。
反模式速查表
| 反模式 | 问题 | 修正方法 |
|---|---|---|
在 Update 中使用 Physics.Raycast,但不设置图层掩码 | 会命中所有内容,包括 UI 碰撞体 | 始终传入 LayerMask 参数 |
每帧都分配新的 RaycastHit[] | 增加 GC 压力 | 使用 NonAlloc 和缓存的缓冲区 |
使用半径为 0 的 OverlapSphere | 不会返回任何结果 | 半径必须大于 0;点检测请使用 CheckSphere |
比较不同查询类型的 hit.distance | SphereCast 距离不等于 Raycast 距离 | SphereCast 距离表示中心移动的距离,而非表面距离 |
使用 maxDistance = Mathf.Infinity | 查询整个场景,开销很大 | 根据使用场景设置合理的最大距离 |
在地面检测中忘记设置 QueryTriggerInteraction.Ignore | 触发器区域会错误地报告“已着地” | 显式传入 QueryTriggerInteraction.Ignore |
相关技能
- unity-physics —— Rigidbody、碰撞体、碰撞/触发器事件、物理设置 API
- unity-3d-math —— 射线投影、Plane 数学、坐标空间
- unity-performance —— 物理查询分析、优化模式