Quick answer: Set Rigidbody.collisionDetectionMode to Continuous (against static geometry) or ContinuousDynamic (against other moving Rigidbodies). For very high velocities, also clamp speed and make colliders thicker than the distance the object can travel in one physics step.
Your bullet or fast-falling player passes clean through a floor and falls out of the world. In the Editor at normal speed it collides fine, but crank up the velocity and it tunnels straight through. This is one of the most commonly reported physics bugs in Unity games, and it comes down to how discrete collision detection works at the boundary of a thin collider.
The Symptom
The Rigidbody works perfectly at low speeds but passes through colliders above a certain velocity threshold. The threshold depends on the collider’s thickness and the fixed physics timestep. A flat floor plane (nearly zero thickness in the collision geometry) will be tunneled through by almost any object moving faster than a few units per second. A floor with a collider several units thick may never exhibit the problem at walking speed but fails reliably for projectiles or during a high-gravity fall.
You might also observe the problem only on lower-end hardware or at lower frame rates, where Unity’s physics simulation may accumulate larger catch-up steps.
What Causes This
Unity PhysX uses a discrete collision detection strategy by default. Every fixed timestep (default 0.02 seconds — 50 Hz), the engine moves all Rigidbodies to their new positions and then checks for overlaps. If an object moves far enough in that single step to completely skip over a collider — entering from one side and exiting the other without any intermediate position ever overlapping the geometry — the physics engine never registers a collision.
This is called tunneling. The maximum safe speed for a given collider thickness is:
// maximum safe speed = collider_thickness / Time.fixedDeltaTime
//
// Example: floor BoxCollider is 0.2 units thick, fixedDeltaTime = 0.02s
// 0.2 / 0.02 = 10 units per second maximum with Discrete mode
//
// A bullet at 80 units/s travels 1.6 units per step.
// Any collider thinner than 1.6 units is tunneled through.
The Fix
Set the collision detection mode
Unity provides four collision detection modes on Rigidbody:
- Discrete — default; only tests at end-of-step positions. Fastest, but tunnels at high speed.
- Continuous — sweeps the moving Rigidbody against static colliders (no Rigidbody). Prevents tunneling through floors, walls, and terrain.
- ContinuousDynamic — sweeps against both static and other dynamic Rigidbodies using
ContinuousorContinuousDynamic. Use for bullets and fast projectiles that must also collide reliably with moving enemies. - ContinuousSpeculative — Unity 2018.3+; a cheaper speculative approach that works for both static and dynamic but can produce ghost contacts on concave shapes.
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class Bullet : MonoBehaviour
{
private Rigidbody rb;
private void Awake()
{
rb = GetComponent<Rigidbody>();
// ContinuousDynamic so this bullet detects collisions
// with both static floors and moving enemies.
rb.collisionDetectionMode =
CollisionDetectionMode.ContinuousDynamic;
rb.useGravity = false;
}
public void Fire(Vector3 direction, float speed)
{
rb.linearVelocity = direction.normalized * speed;
}
}
For the player character (falls fast but is not a projectile), Continuous is usually sufficient and cheaper than ContinuousDynamic:
private void Awake()
{
rb = GetComponent<Rigidbody>();
// Prevents falling through floors; the player doesn't need
// continuous collision against other dynamic bodies.
rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
}
Clamp maximum velocity
Regardless of detection mode, setting an upper bound on velocity prevents edge cases caused by physics explosions (two colliders overlapping due to a bug, pushing each other at enormous speed) and keeps the simulation stable:
using UnityEngine;
public class VelocityClamp : MonoBehaviour
{
[SerializeField] private float maxSpeed = 40f;
private Rigidbody rb;
private void Awake() => rb = GetComponent<Rigidbody>();
private void FixedUpdate()
{
if (rb.linearVelocity.magnitude > maxSpeed)
rb.linearVelocity = rb.linearVelocity.normalized * maxSpeed;
}
}
Make colliders thicker
For static level geometry, the cheapest fix is to make the collider thicker. Use a BoxCollider instead of a flat MeshCollider and extend it downward so it is at least 0.5–1.0 units thick. Offset the center to keep the top surface flush with the visual mesh:
private void OnValidate()
{
var box = GetComponent<BoxCollider>();
if (box == null) return;
// Top surface stays at local Y=0, collider extends 1 unit down.
box.size = new Vector3(box.size.x, 1.0f, box.size.z);
box.center = new Vector3(box.center.x, -0.5f, box.center.z);
}
Configure the layer collision matrix
Continuous detection has a per-pair cost. Only enable it for layers that actually interact at high speed. In Edit › Project Settings › Physics, open the Layer Collision Matrix and disable collisions between pairs that never need to interact (bullets vs. decorative props, for example). In physics queries, always pass a layer mask to avoid evaluating irrelevant layers:
private void CheckGrounded()
{
// Only query the Environment layer (layer index 6)
int environmentMask = 1 << 6;
bool isGrounded = Physics.Raycast(
transform.position,
Vector3.down,
0.15f,
environmentMask
);
}
Physics.queriesHitBackfaces
By default, Physics.Raycast and related queries only detect the front face of a collider. If you need a bullet to detect the exit point of a solid object (for penetration effects or tunneling detection logic), enable backface hits:
private void Awake()
{
// Enable back-face hit detection globally.
// Useful for projectile penetration raycasts.
Physics.queriesHitBackfaces = true;
}
This is a global setting. It increases the number of hits returned by raycasts and has a small performance cost, so consider keeping it scoped to specific queries by toggling it on and off around the calls that need it.
Related Issues
- Character falls through floor on scene load. Often caused by the floor mesh being loaded before the collider bakes, especially for procedurally generated terrain. Delay spawning the character by one frame or call
Physics.SyncTransforms()after building the mesh. - Rigidbody jitters on a moving platform. When both the platform and the character have Rigidbodies and you use
ContinuousDynamic, speculative contacts can cause small jitters. Prefer parenting the character to the platform via a script, or useContinuousSpeculativeon the character only. - Performance regression after enabling Continuous. Continuous and ContinuousDynamic modes are significantly more expensive than Discrete. Profile in the Physics section of the Unity Profiler and apply the modes only to objects that genuinely need them — do not set
ContinuousDynamicon every Rigidbody in the scene.
As a rule of thumb: Continuous for players and important items, ContinuousDynamic for bullets, Discrete for everything else — and make your floors at least 0.5 units thick.