unity-physics-queries
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):
// Always defaulting to Raycast for everything
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):
// 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
}
RIGHT:
// 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);
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);
// Assuming hits[0] is the closest
ProcessHit(hits[0]);
RIGHT:
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
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):
// Buffer of 1 -- silently drops extra results
RaycastHit[] buffer = new RaycastHit[1];
int count = Physics.RaycastNonAlloc(ray, buffer, maxDist);
RIGHT:
// 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");
}
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):
// 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
RIGHT:
// 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");
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):
// Assuming triggers are ignored by queries
if (Physics.Raycast(origin, direction, out hit, maxDist, layerMask))
{
// hit.collider might be a trigger!
}
RIGHT:
// 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
}
}
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):
// 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
RIGHT:
// 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)
}
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):
// Using center + full height
Physics.CapsuleCast(center, center + Vector3.up * height, radius, direction, out hit);
RIGHT:
// 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);
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):
// 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
}
RIGHT:
// 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
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):
// Global queries search ALL physics scenes
Collider[] results = Physics.OverlapSphere(center, radius);
RIGHT:
// 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);
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