Collision Detection and Trigger System Development
OnTriggerEnter fired twice in a row—player got item twice. OnCollisionEnter doesn't fire at all—forgot to put Rigidbody on one object. Trigger zone reacts to projectiles, debris, and NPCs when should only react to player. Not engine bugs—these are natural consequences of working with collisions without understanding architecture.
How Collisions Work in Unity: Basics Without Simplification
Unity PhysX divides interactions into two types: collision (physical contact with reaction) and trigger (intersection detection without physical response).
OnCollisionEnter(Collision) is called if both objects are not triggers and at least one has Rigidbody (not kinematic). Collision contains ContactPoint[] with contact points, normals, and relative velocity—useful for impact sound, particle spawning.
OnTriggerEnter(Collider) is called if one object is a trigger (isTrigger = true). The collider passed as parameter is the entering object, not the trigger itself. Nuance: if both objects are triggers, event still fires (in Unity 2022+), but no physical response.
Call Matrix:
| Object A | Object B | Event |
|---|---|---|
| Rigidbody + Collider | Collider (Static) | OnCollisionEnter on A |
| Rigidbody + Trigger | Collider (Static) | OnTriggerEnter on A |
| Rigidbody + Collider | Rigidbody + Collider | OnCollisionEnter on both |
| Kinematic RB + Trigger | Rigidbody + Collider | OnTriggerEnter on both |
| Static Collider | Static Collider | Nothing |
The last row is the most common problem: two static colliders without Rigidbody never fire collision events.
Trigger Double-Fire Problem
OnTriggerEnter can fire multiple times for one entry if object has multiple colliders (compound collider). Each child collider fires OnTriggerEnter on the trigger on entry.
Protection—flag or HashSet:
private bool _activated = false;
private void OnTriggerEnter(Collider other)
{
if (_activated) return;
if (!other.CompareTag("Player")) return;
_activated = true;
ActivateTrigger();
}
For reusable triggers (e.g., damage zone): HashSet<int> with InstanceID of objects inside zone—add on OnTriggerEnter, remove on OnTriggerExit. Apply damage only to objects in HashSet, update via InvokeRepeating tick.
Trigger System Architecture for Levels
Monolithic OnTriggerEnter with long switch by tags—bad architecture. Adding new interaction type requires editing one huge component.
Better approach—Event Trigger pattern:
public class TriggerZone : MonoBehaviour
{
public UnityEvent<Collider> OnEntered;
public UnityEvent<Collider> OnExited;
private void OnTriggerEnter(Collider other) => OnEntered?.Invoke(other);
private void OnTriggerExit(Collider other) => OnExited?.Invoke(other);
}
TriggerZone is dumb dispatcher. Logic connects outside via inspector or AddListener() from other components. Want door to open—connect Door.Open to OnEntered. Want enemy spawn—connect EnemySpawner.Spawn. No need to touch TriggerZone when adding new actions.
For filtering by object type: not tags (CompareTag—string comparison, slow with many)—but layers: if (other.gameObject.layer == LayerMask.NameToLayer("Player")). Even better—cache int _playerLayer = LayerMask.NameToLayer("Player") in Awake().
Raycast and OverlapSphere: When Trigger Colliders Don't Fit
Some collision detection tasks solve not via OnTriggerEnter but explicit physics queries:
Physics.Raycast detects in a ray. Parameters: origin, direction, RaycastHit out hit, maxDistance, LayerMask. Important: if ray starts inside collider, that collider won't be detected. For melee weapons where hitbox can partially overlap own collider—offset origin 0.1f backward along direction.
Physics.SphereCastAll is volume query along trajectory. Returns RaycastHit[] with all intersected objects. Used for weapon hitbox with thickness (sword strike—not point, but volume). More performant than OverlapSphere at path end + raycast at start.
Physics.OverlapSphere / Physics.OverlapBox return all Collider[] in zone without contact info. For enemies in explosion zone detection, item pickup, AI perception. Result written to preallocated buffer via Physics.OverlapSphereNonAlloc(center, radius, results, mask)—variant without GC allocation, critical for per-frame calls.
Optimization: QueryTriggerInteraction
By default physics queries (Raycast, OverlapSphere) can hit triggers. Controlled via QueryTriggerInteraction parameter:
-
UseGlobalfollowsPhysics.queriesHitTriggerssetting -
Collidehits triggers -
Ignoreignores triggers
For bullets that should hit wall colliders but not interactive trigger zones: Physics.Raycast(ray, out hit, dist, mask, QueryTriggerInteraction.Ignore).
Timeline Guidelines
| Task | Timeframe |
|---|---|
| Basic trigger zones for level | 1–2 days |
| Event trigger system (TriggerZone + UnityEvent) | 2–4 days |
| Hitbox/hurtbox system for combat | 3–7 days |
| Full detection system (FOV + OverlapSphere + Raycast) | 1–2 weeks |
Common Mistakes
Don't cache LayerMask.NameToLayer() result—string lookup, expensive if called in Update(). Cache in Awake().
Use tag instead of layer for query filtering—tags aren't filtered at PhysX level, checked after collecting all results.
OnTriggerStay every frame without Time.deltaTime—source of unpredictable damage zone behavior: damage applied depending on fps, not game time. Always damage * Time.deltaTime or tick via InvokeRepeating.





