| name | unity-physics-queries |
| description | Unity physics query correctness patterns. Catches common mistakes with Raycast, SphereCast, OverlapSphere, NonAlloc allocation, LayerMask construction, trigger interaction, hit ordering, and query type selection. PATTERN format: WHEN/WRONG/RIGHT/GOTCHA. Based on Unity 6.3 LTS.
|
| globs | ["**/*.cs"] |
Physics Query Patterns -- Correctness Patterns
Prerequisite skills: unity-physics (Rigidbody, colliders, raycasting API), unity-foundations (layers, GameObjects)
These patterns target the most common physics query bugs: using the wrong query type, misunderstanding allocation, and ignoring subtle defaults that cause silent failures.
PATTERN: Query Type Selection
WHEN: Choosing which physics query to use
WRONG (Claude default):
Physics.Raycast(origin, direction, out hit, maxDistance);
RIGHT -- use the decision tree:
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)
GOTCHA: Raycast only returns the closest hit. If you need to pierce through multiple objects, use RaycastAll or RaycastNonAlloc. If you need to detect everything in an area (like an explosion radius), OverlapSphere is correct -- NOT SphereCast.
PATTERN: Cast Origin Inside Collider
WHEN: A SphereCast/BoxCast/CapsuleCast starts overlapping an existing collider
WRONG (Claude default):
if (Physics.SphereCast(feetPosition, radius, Vector3.down, out hit, 0.1f))
{
grounded = true;
}
RIGHT:
grounded = Physics.CheckSphere(feetPosition, radius, groundMask);
Collider[] touching = Physics.OverlapSphere(feetPosition, radius, groundMask);
GOTCHA: This applies to ALL cast queries (SphereCast, BoxCast, CapsuleCast, Raycast). If the origin is inside a collider, that collider is ignored. This is the #1 source of "my ground check doesn't work" bugs. Raycasts that start inside a MeshCollider also miss it. Use Overlap* or Check* for current-overlap detection.
PATTERN: Hit Ordering Not Guaranteed
WHEN: Using RaycastAll or RaycastNonAlloc and expecting sorted results
WRONG (Claude default):
RaycastHit[] hits = Physics.RaycastAll(origin, direction, maxDist);
ProcessHit(hits[0]);
RIGHT:
RaycastHit[] hits = Physics.RaycastAll(origin, direction, maxDist);
System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
if (hits.Length > 0)
ProcessHit(hits[0]);
GOTCHA: Regular Physics.Raycast (single hit) always returns the closest. Only RaycastAll and RaycastNonAlloc return unsorted results. The same applies to SphereCastAll/SphereCastNonAlloc, etc. For NonAlloc, sort only up to the returned count, not the full buffer.
PATTERN: NonAlloc Buffer Size and Return Count
WHEN: Using RaycastNonAlloc, OverlapSphereNonAlloc, or similar zero-allocation queries
WRONG (Claude default):
RaycastHit[] buffer = new RaycastHit[1];
int count = Physics.RaycastNonAlloc(ray, buffer, maxDist);
RIGHT:
private readonly RaycastHit[] _hitBuffer = new RaycastHit[16];
void DetectHits()
{
int count = Physics.RaycastNonAlloc(ray, _hitBuffer, maxDist, layerMask);
for (int i = 0; i < count; i++)
{
ProcessHit(_hitBuffer[i]);
}
if (count == _hitBuffer.Length)
Debug.LogWarning("Hit buffer full -- may have missed results");
}
GOTCHA: NonAlloc fills the provided buffer and returns how many results were written. If there are more results than buffer capacity, extras are silently dropped with no error. Size your buffer to the maximum expected results for your use case. Common sizes: ground check = 4, explosion radius = 32, broad scan = 64.
PATTERN: LayerMask Bitshift vs GetMask
WHEN: Constructing a layer mask for physics queries
WRONG (Claude default):
int mask = 1 << LayerMask.GetMask("Ground");
RIGHT:
int groundMask = LayerMask.GetMask("Ground");
int multiMask = LayerMask.GetMask("Ground", "Water", "Default");
int groundLayer = LayerMask.NameToLayer("Ground");
int groundMask2 = 1 << groundLayer;
int combinedMask = (1 << LayerMask.NameToLayer("Ground")) | (1 << LayerMask.NameToLayer("Water"));
int everythingButGround = ~LayerMask.GetMask("Ground");
GOTCHA: LayerMask.GetMask("Ground") = bitmask (e.g., 256). LayerMask.NameToLayer("Ground") = index (e.g., 8). gameObject.layer = index. Passing a layer index where a mask is expected (or vice versa) silently filters wrong layers with no error.
PATTERN: QueryTriggerInteraction Default
WHEN: Raycasts or other queries are unexpectedly hitting trigger colliders
WRONG (Claude default):
if (Physics.Raycast(origin, direction, out hit, maxDist, layerMask))
{
}
RIGHT:
if (Physics.Raycast(origin, direction, out hit, maxDist, layerMask, QueryTriggerInteraction.Ignore))
{
}
if (Physics.Raycast(origin, direction, out hit, maxDist, layerMask))
{
if (!hit.collider.isTrigger)
{
}
}
GOTCHA: The default is QueryTriggerInteraction.UseGlobal, which reads from Physics.queriesHitTriggers. That global default is true -- meaning queries DO hit triggers by default. This catches many developers off guard. Set it explicitly when trigger hits would cause bugs (ground checks, line-of-sight, bullet traces).
PATTERN: SphereCast Radius vs Distance
WHEN: Using SphereCast and confusing the parameters
WRONG (Claude default):
Physics.SphereCast(origin, detectionRange, direction, out hit);
RIGHT:
float sphereRadius = 0.5f;
float castDistance = 10f;
if (Physics.SphereCast(origin, sphereRadius, direction, out hit, castDistance, layerMask))
{
}
GOTCHA: hit.distance is the distance the sphere's center traveled before contact, NOT the total distance from origin to the hit surface. The actual contact surface is at hit.point. A SphereCast with radius=0 behaves like a Raycast. If the sphere is very large and the cast distance is short, you may miss nearby objects due to the "origin inside collider" issue.
PATTERN: CapsuleCast Point Parameters
WHEN: Setting up CapsuleCast endpoints
WRONG (Claude default):
Physics.CapsuleCast(center, center + Vector3.up * height, radius, direction, out hit);
RIGHT:
float height = 2.0f;
float radius = 0.5f;
Vector3 point1 = center + Vector3.up * (height * 0.5f - radius);
Vector3 point2 = center - Vector3.up * (height * 0.5f - radius);
Physics.CapsuleCast(point1, point2, radius, direction, out hit, maxDistance, layerMask);
GOTCHA: The total capsule height = |point2 - point1| + 2 * radius. If point1 == point2, it degenerates into a SphereCast. The CapsuleCollider component defines this differently (center + height + radius), so translating from a CapsuleCollider requires: point1 = center + up * (height/2 - radius), point2 = center - up * (height/2 - radius).
PATTERN: Backface Detection
WHEN: Raycasting against MeshColliders from behind
WRONG (Claude default):
if (Physics.Raycast(insidePoint, direction, out hit))
{
}
RIGHT:
Physics.queriesHitBackfaces = true;
GOTCHA: By default, Physics.queriesHitBackfaces = false. This only affects non-convex MeshColliders. Box, Sphere, Capsule, and convex MeshColliders detect hits from any direction. If you need to raycast from inside a non-convex mesh (e.g., room interior), either enable backface queries or use a convex collider for the interior.
PATTERN: Scene-Specific Queries
WHEN: Using additive scenes with separate physics simulations
WRONG (Claude default):
Collider[] results = Physics.OverlapSphere(center, radius);
RIGHT:
PhysicsScene physScene = gameObject.scene.GetPhysicsScene();
RaycastHit hit;
if (physScene.Raycast(origin, direction, out hit, maxDist, layerMask))
{
}
Collider[] buffer = new Collider[32];
int count = physScene.OverlapSphere(center, radius, buffer, layerMask);
GOTCHA: By default, all scenes share the same Physics.defaultPhysicsScene. Scene-specific physics only matters when you explicitly create scenes with LocalPhysicsMode.Physics3D. Most projects never need this -- but it's critical for multiplayer prediction, parallel simulations, or editor preview scenes.
Anti-Patterns Quick Reference
| Anti-Pattern | Problem | Fix |
|---|
Physics.Raycast in Update without layer mask | Hits everything including UI colliders | Always pass a LayerMask parameter |
Allocating new RaycastHit[] every frame | GC pressure | Use NonAlloc with a cached buffer |
OverlapSphere with radius 0 | Returns nothing | Radius must be > 0; use CheckSphere for point checks |
Comparing hit.distance across different query types | SphereCast distance != Raycast distance | SphereCast distance is center travel, not surface distance |
Using maxDistance = Mathf.Infinity | Queries entire scene, expensive | Use a reasonable max distance for your use case |
Forgetting QueryTriggerInteraction.Ignore on ground checks | Trigger volumes falsely report "grounded" | Pass QueryTriggerInteraction.Ignore explicitly |
Related Skills
- unity-physics -- Rigidbody, colliders, collision/trigger events, physics settings API
- unity-3d-math -- Raycasting projection, Plane math, coordinate spaces
- unity-performance -- Profiling physics queries, optimization patterns
Additional Resources