Quick answer: Set the moving Rigidbody’s Collision Detection to Continuous (vs static triggers) or Continuous Dynamic (vs moving Rigidbody triggers). Discrete misses fast movers.
A bullet flies through a damage trigger volume. Sometimes the damage applies, sometimes the bullet passes clean through with no OnTriggerEnter event. The bullet’s speed is well within game logic ranges, but the trigger is being tunneled at high frame deltas.
Why Discrete Misses Triggers
Unity’s default collision mode is Discrete. At each fixed-update tick, the physics engine checks if a Rigidbody’s collider overlaps any other collider. If the Rigidbody moved fast enough between ticks to be on one side of a trigger at tick N and the other side at tick N+1, no overlap is detected at either tick, so no OnTriggerEnter fires.
The fixed timestep defaults to 0.02 seconds (50 Hz). At 50 m/s, a body moves 1 m per tick. If a trigger’s thickness along the motion vector is less than 1 m, tunneling happens.
The Fix: Continuous Collision Detection
On the fast Rigidbody:
GetComponent<Rigidbody>().collisionDetectionMode = CollisionDetectionMode.Continuous;
Or set it in the Inspector under the Rigidbody component. The four modes:
- Discrete — default, cheap, miss-prone above ~1 collider thickness per tick.
- Continuous — CCD against static colliders and kinematic Rigidbodies. Use for projectiles and fast objects hitting world geometry.
- Continuous Dynamic — CCD against all other CCD-enabled bodies too. Use when two fast bodies must collide accurately with each other.
- Continuous Speculative — cheaper alternative using speculative contacts; works for both static and dynamic but less accurate.
For triggers specifically — the static trigger volume doesn’t need any setting. The flag is on the moving body.
Required Setup Quirks
CCD has caveats:
- The Rigidbody must not be Kinematic. Kinematic bodies don’t use CCD at all; they teleport.
- The moving collider should be Convex on a MeshCollider, or a primitive (Sphere/Box/Capsule). Concave MeshColliders on Rigidbodies aren’t supported.
- Continuous Dynamic only matters between two CCD-enabled Rigidbodies; one Discrete and one Continuous Dynamic still misses each other.
Alternate Fix: Replace Trigger with OverlapBox
For one-shot collision points (e.g., a projectile’s impact check), skip the trigger and use a swept query:
void FixedUpdate()
{
Vector3 from = transform.position;
Vector3 to = from + rigidbody.linearVelocity * Time.fixedDeltaTime;
if (Physics.SphereCast(from, radius, rigidbody.linearVelocity.normalized,
out RaycastHit hit, (to - from).magnitude, triggerMask, QueryTriggerInteraction.Collide))
{
OnHitTrigger(hit.collider);
}
}
QueryTriggerInteraction.Collide tells SphereCast to include trigger colliders in its hits. This gives you the exact contact point and surface normal — better than OnTriggerEnter, which only tells you that a collider overlaps.
Increase Fixed Tick Rate
For project-wide improvement, lower the fixed timestep:
// Edit → Project Settings → Time
// Fixed Timestep: 0.01 (100 Hz, default 0.02)
Doubling the tick rate halves per-tick travel distance. Cost is double physics CPU; helpful if tunneling appears in many places.
Verifying
Add a counter for triggers entered. Launch the projectile through a thin trigger volume 100 times at varying speeds. Before the fix, miss rate scales with speed. After enabling Continuous, every pass registers an OnTriggerEnter.
“Discrete + Fast + Thin = Missed Event. Pick one to change. Continuous CD is usually the cheapest fix.”
Set Continuous per-prefab on projectiles, not project-wide — CCD cost adds up across hundreds of bodies.