Which pairs of objects actually report contact and which stay silent: the Rigidbody rule, the trigger switch, the difference between OnCollisionEnter and OnTriggerEnter, and why CompareTag beats comparing tag strings.
Subject: Unity Game Engine · 62 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Unity - Lesson 8
Which pairs of objects talk to each other - and which overlap in complete silence.
Objectives
The diagnostic got 2 of 3 here, both with low confidence. That pattern - right answers, no certainty - is what this deck is for: a rule you can derive beats one you half remember.
OnCollisionEnter and OnTriggerEnter you will get, before running itCompareTag, and say why the == version is worseWarm-up
Before anything else.
Discussion prompt
A ball rolls into a wall. Both have colliders. Under what circumstances does your OnCollisionEnter get called - and on which of the two objects?
Hint: One of the two objects has to be doing something the other is not.
Answer:
At least one of the pair needs a Rigidbody. Normally that is the ball; the wall has a collider and nothing else, and that is correct level design.
Both objects get the callback. Unity delivers it to both sides of the pair, so the script can live on either - which surprises people who assume only the moving one is told.
If neither has a Rigidbody, nothing happens at all. That is the row worth remembering.
Section
Part 1
Concept
A Collider defines the volume the physics engine tests against. It does not draw anything, does not move anything, and does not make an object physical - lesson 3's Rigidbody does that.
Collider — A shape attached to a GameObject, used to test whether things touch. Independent of the mesh you see and independent of whether the object is simulated.
The collider and the visible mesh are separate. A capsule collider on a detailed character model is normal and desirable - the shape is for physics, not for looks.
Unity Manual - Colliders Colliders
Concept
| collider | shape | cost | use for |
|---|---|---|---|
| Box | a box | cheapest | crates, walls, most scenery |
| Sphere | a ball | cheapest | balls, blast radii, pickups |
| Capsule | a pill | cheap | characters - it does not catch on steps |
| Mesh | the actual mesh | expensive | complex static geometry only |
| Compound (several primitives) | whatever you build | cheap | an L-shape, a chair, a vehicle |
A Mesh Collider cannot be dynamic unless you tick Convex, and even then it is the last resort. Building a shape out of two or three boxes is faster in every sense - to compute, and to reason about.
Prediction
A cube with a Box Collider and its Mesh Renderer removed.
Predict first
What is it now?
Correct: An invisible object that things still bump into.
Why: Being drawn and being solid are separate capabilities from separate components - lesson 1's model, applied. Every invisible boundary in every game you have played is exactly this: a collider with no renderer.
Concept
Figure (svg): Three lanes showing a dynamic body hitting a solid wall, passing through a trigger, and two static colliders overlapping in silence
Whether anything is reported when two colliders meet depends on exactly two questions.
OnTrigger* and nothing is stopped. No - you get OnCollision* and things are stopped.Everything else in this lesson is a consequence of those two lines.
Unity Manual - Collision action matrix Collision action matrix
Sorting
Sort into buckets
Collider, or no collider?
Worked example
The lab describes each object in the two terms that matter.
public string Describe()
{
Collider shape = GetComponent<Collider>();
Rigidbody body = GetComponent<Rigidbody>();
string kind = shape == null ? "no collider"
: shape.isTrigger ? "trigger collider" : "solid collider";
string physics = body == null ? "no Rigidbody"
: body.isKinematic ? "kinematic Rigidbody" : "dynamic Rigidbody";
return $"{kind} + {physics}";
}| object | Describe() returns | what it can do |
|---|---|---|
| Ball A | solid collider + dynamic Rigidbody | moves, is stopped, reports |
| Solid Wall | solid collider + no Rigidbody | stops things, reports, never moves |
| Trigger Zone | trigger collider + no Rigidbody | detects, never stops |
| Static X | solid collider + no Rigidbody | nothing happens with Static Y |
Two words describe every object in the matrix
Why: kind of collider, and kind of body. Once you can say those two for both objects in a pair, the outcome is determined.
Explain it to yourself
Discussion prompt
Two cubes with colliders and no Rigidbodies are overlapping. Explain why Unity fires no callback - and why that is a deliberate design choice rather than an oversight.
Answer:
Because neither is being simulated. Static colliders are baked into a structure the engine uses to answer 'what is near this moving thing?', and static-against-static pairs are never queried at all.
The reason is cost. A level has thousands of static colliders; testing every pair of them every frame would be enormous work to discover something that by definition never changes - they are static, so their overlap is the same this frame as last.
Trap
Two objects, two colliders, overlapping. Something should fire.
Static X BoxCollider, no Rigidbody
Static Y BoxCollider, no Rigidbody
They overlap by half a metre.
OnCollisionEnter on X: never called
OnCollisionEnter on Y: never calledNo error, no warning, no log line. The most confusing kind of failure: nothing at all.
Give one of them a Rigidbody, and pick the kind that matches what it should do.
Static X BoxCollider, no Rigidbody (still fine as scenery)
Mover Y BoxCollider + Rigidbody (dynamic: falls and is pushed)
or BoxCollider + kinematic Rigidbody, moved with MovePosition
Now both objects receive OnCollisionEnter.| what Y should do | give it |
|---|---|
| fall and be pushed around | a dynamic Rigidbody |
| move exactly as you script it | a kinematic Rigidbody + MovePosition |
| never move, but be detected | leave it static and make the OTHER one the mover |
Section
Part 2
Concept
Tick Is Trigger on a collider and it stops resolving contacts. Things pass straight through it, and instead of OnCollision* you get OnTrigger*.
That is the whole feature: a volume that notices you without stopping you. Checkpoints, pickups, damage zones, doors that open, cutscene starts - all triggers.
Unity Scripting API - MonoBehaviour.OnTriggerEnter OnTriggerEnter
Intuition
A door stops you and you feel it. A doorway with a sensor above it notices you and does nothing to your movement.
Both are 'the same shape in the same place'. The difference is entirely in whether anything is pushed back - and Unity spells that difference isTrigger.
Prediction
A ball with a dynamic Rigidbody rolls into a box whose collider has Is Trigger ticked. The ball's script defines both OnCollisionEnter and OnTriggerEnter.
Predict first
Which one runs?
Correct: OnTriggerEnter only, and the ball keeps going.
Why: A trigger anywhere in the pair means the whole interaction is a trigger interaction. There is no collision to report, because nothing was resolved - which is exactly why the parameter is a Collider and not a Collision. This is the first misconception the diagnostic recorded.
Concept
Figure (svg): Two callback signatures side by side showing Collision carries contact data and Collider does not
Six callbacks, two families of three. The parameter type tells you which family you are in.
Unity Scripting API - MonoBehaviour.OnCollisionEnter OnCollisionEnter
Worked example
Get the type wrong and Unity never calls your method - with no error.
void OnCollisionEnter(Collision collision)
{
GameObject other = collision.gameObject;
int points = collision.contactCount; // where they touched
Vector3 impact = collision.relativeVelocity; // how hard
}
void OnTriggerEnter(Collider other)
{
GameObject them = other.gameObject;
// no contact points, no impulse: nothing was resolved
}| you have | OnCollisionEnter | OnTriggerEnter |
|---|---|---|
| the other GameObject | collision.gameObject | other.gameObject |
| contact points | yes | no |
| relative velocity | yes | no |
| was anything stopped? | yes | no |
| parameter type | Collision | Collider |
The parameter type is the tell
Why: Collision describes a resolution - what was stopped, where, how hard. Collider is just the shape that entered.
Matching
Match the pairs
Which callback for each job?
Why: Only the first needs a real collision, because only it needs the impact data - relativeVelocity is what scales the sound. The other three are volumes that detect without blocking, and the Enter/Stay/Exit trio covers arriving, remaining and leaving.
Concept
Each family has three moments, and Stay is the expensive one.
| callback | fires | typical use | watch out |
|---|---|---|---|
*Enter | once, on arrival | pickups, damage on hit, doors | - |
*Stay | every physics step while overlapping | damage over time, standing checks | runs ~50x a second, per pair |
*Exit | once, on leaving | closing doors, leaving a zone | also fires if the object is destroyed |
Put nothing expensive in a Stay callback. A lava zone should accumulate damage * Time.fixedDeltaTime, not run a search or spawn an object every step.
Error analysis
A pickup that never fires. Everything looks right.
Annotate
A useful habit: let the IDE generate these. Typing OnTrigger and accepting the completion gets the signature right every time.
Discrimination
Sort into buckets
Should this collider have Is Trigger ticked?
Fill the middle
Fill in the blanks
void OnTriggerEnter(Collider other)
CompareTag}("Player")) return;
score += 1;
Destroy(gameObject);
}
Why: Collider is the parameter type for every trigger callback, and getting it wrong means Unity silently never calls the method. CompareTag is checked against the project's tag list, so a tag that does not exist throws instead of quietly returning false - which is the next part of this lesson.
Section
Part 3
Concept
Every pair of colliders falls into one of these. The lab derives the whole table from one scene.
| object A | object B | what fires |
|---|---|---|
| solid + Rigidbody | solid, no Rigidbody | OnCollision* on both |
| solid + Rigidbody | solid + Rigidbody | OnCollision* on both |
| solid + Rigidbody | trigger, no Rigidbody | OnTrigger* on both |
| trigger + Rigidbody | solid, no Rigidbody | OnTrigger* on both |
| solid, no Rigidbody | solid, no Rigidbody | nothing |
| kinematic Rigidbody | solid, no Rigidbody | nothing |
| kinematic Rigidbody | trigger, no Rigidbody | OnTrigger* fires |
Two shorthand rules cover almost all of it: a Rigidbody must be somewhere in the pair, and a trigger anywhere in the pair makes it a trigger interaction.
Prediction
Two cubes with Box Colliders, overlapping, neither with a Rigidbody. Both have a script implementing every collision and trigger callback.
Predict first
How many callbacks fire?
Correct: None at all.
Why: Neither is simulated, so the engine never tests that pair. This is the second misconception the diagnostic recorded, and it is the hardest to spot because there is no error to search for - the code is correct, the colliders are correct, and nothing happens.
Worked example
The lab's three lanes, and the summary it prints after four seconds.
foreach (ContactReporter reporter in ContactReporter.All)
Debug.Log($"{reporter.name} | {reporter.Describe()} | " +
$"collision enters {reporter.CollisionEnters} | " +
$"trigger enters {reporter.TriggerEnters}");| object | kind | collision enters | trigger enters |
|---|---|---|---|
| Ball A | solid + dynamic | 1 | 0 |
| Solid Wall | solid + none | 1 | 0 |
| Ball B | solid + dynamic | 0 | 1 |
| Trigger Zone | trigger + none | 0 | 1 |
| Static X | solid + none | 0 | 0 |
| Static Y | solid + none | 0 | 0 |
Read it in pairs
Why: Both members of a pair report the same number, or both report zero. A callback is never delivered to only one side.
Invariant
Step through the three lanes and find the thing that is true in every one.
Step through it
What is constant across all three lanes, and what does it mean for where you put your script?
Both sides always agree. So the script can go on either object - put it wherever the logic belongs, not on 'the moving one'.
Comparison
Comparison matrix
| A \ B | solid, no Rigidbody | trigger, no Rigidbody |
|---|---|---|
| solid + dynamic Rigidbody | OnCollision* | OnTrigger* |
| solid, no Rigidbody | nothing | nothing |
| kinematic Rigidbody | nothing | OnTrigger* |
The bottom-right cell is worth remembering on its own: a kinematic body does fire triggers, which is what makes scripted movers usable with detection zones.
Trap
Nothing is firing, and the rule says I need a Rigidbody. I will add one to the wall.
Wall: BoxCollider + Rigidbody
Result: the wall falls through the floor, the level collapses,
and the callbacks now fire on the way down.The rule was satisfied and the level was destroyed. The Rigidbody belongs on the thing that should move.
Put the Rigidbody on the mover. Scenery stays as a plain collider - that is what static geometry is.
Wall: BoxCollider, no Rigidbody <- correct, and it stays put
Ball: BoxCollider + Rigidbody <- the mover carries the requirement
Both objects now receive OnCollisionEnter.| if nothing fires, ask | not |
|---|---|
| is anything in this pair actually moving? | should I add more Rigidbodies? |
| does the mover have the Rigidbody? | does the wall need one? |
| is either one a trigger? | why is OnCollisionEnter not called? |
Concept
Edit > Project Settings > Physics has a checkbox grid: which layers may collide with which. Objects on layers whose box is unticked never interact, whatever their colliders say.
| use case | how |
|---|---|
| bullets should not hit the shooter | put them on separate layers, untick that pair |
| UI raycasts should ignore the world | layer + LayerMask on the raycast |
| performance: lots of debris | a Debris layer that only collides with the ground |
It is also the first thing to check when a collision mysteriously stops working after someone else touched the project - the setting is global, invisible from the objects, and not in any script.
Two truths and a lie
Eliminate the wrong options
Which claim about collision callbacks is false?
Survives elimination: B
Why: Both objects in the pair receive the callback, which is why your script can live on either one. That symmetry is genuinely useful: a damage zone can hold the logic, or the player can, and both designs work with no change to the scene.
Section
Part 4
Concept
A tag is a short label on a GameObject, set at the top of the Inspector. CompareTag is how you check one.
void OnTriggerEnter(Collider other)
{
if (!other.CompareTag("Player")) return;
// ...it really was the player
}| approach | allocates? | typo behaviour |
|---|---|---|
other.CompareTag("Player") | no | throws if the tag is not defined |
other.tag == "Player" | yes - a string per call | silently returns false forever |
other.name == "Player" | yes | breaks on 'Player(Clone)' |
Unity Scripting API - GameObject.CompareTag CompareTag
Prediction
You write other.CompareTag("Enemy") but never created an Enemy tag in Tags & Layers.
Predict first
What happens?
Correct: It throws: UnityException: Tag: Enemy is not defined.
Why: That is CompareTag being helpful. A tag you never defined is always a mistake, and an exception naming it takes seconds to fix - whereas tag == "Enemy" would return false forever and leave you debugging the collision instead of the typo.
Trap
The parameter is the thing I hit, so compare it to what I am looking for.
void OnCollisionEnter(Collision collision)
{
if (collision == "Player") { ... } // does not compile
if (collision.gameObject.name == "Player") { ... } // compiles, fragile
}| problem | why |
|---|---|
| comparing a Collision to a string | different types - a Collision is not a name |
| comparing name | spawned objects are called 'Player(Clone)' |
| comparing name | renaming an object in the Hierarchy breaks the logic |
comparing tag with == | a typo fails silently, and it allocates |
Reach the GameObject, then ask it a question with CompareTag - or better, ask for a component.
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Player")) { ... }
// usually better: ask what it CAN DO, not what it is called
if (collision.gameObject.TryGetComponent(out Health health))
health.TakeDamage(10);
}The component check scales further than tags: anything with a Health component can be damaged, whether it is a player, an enemy or a barrel, and no string is involved at all.
Discrimination
Three ways to identify what you hit. Sort each situation.
Sort into buckets
Which identification method fits?
Worked example
Everything from this lesson, in one small script.
public class Coin : MonoBehaviour
{
public int value = 1;
bool collected; // guards the double-fire
void OnTriggerEnter(Collider other)
{
if (collected) return;
if (!other.CompareTag("Player")) return;
collected = true;
ScoreBoard.Add(value);
Destroy(gameObject);
}
}| line | which lesson it came from |
|---|---|
OnTriggerEnter(Collider other) | 8 - trigger family, correct signature |
if (collected) return; | 5 - Destroy is deferred, so it can fire twice |
CompareTag | 8 - allocation-free, throws on a typo |
Destroy(gameObject) | 5 - the object, not the component |
| Is Trigger ticked on the collider | 8 - detect without blocking |
The collected flag is not paranoia
Why: The coin survives to the end of the frame, so a second overlap in that same frame finds a live coin with a live script. Lesson 5's timing, showing up as a scoring bug.
Notation
Annotate
Counterexample
Discussion prompt
'If OnTriggerEnter fires, OnTriggerExit will fire too.' Find a case where it does not.
Hint: What if one of the two objects stops existing?
Answer:
Destroy or deactivate either object while they overlap and Exit may never arrive - the pair stops existing rather than separating.
Same for a collider being disabled, or the object being teleported. So any code that pairs an Enter with an Exit - a counter of who is inside a zone, a door that closes when empty - needs to handle objects vanishing, usually by cleaning the list in OnDisable or by validating entries before use.
Section
Build
Concept
Three lanes that between them cover every row of the matrix, and a report that prints who heard what.
Tags used are Player and Finish - both exist in a new project, so nothing needs defining first. Files: unity-labs/Assets/NaruhodoLabs/Lesson08_Collisions/.
Step zero
Discussion prompt
What is the smallest set of objects that demonstrates all three outcomes - collision, trigger, and silence? List them with their two properties each.
Answer:
The sixth object matters: without a script on the silent pair you have not shown they heard nothing, only that you did not ask.
Worked example
Lane 1 stops; lane 2 passes through.
Ball A at (-4, 1, -4), Tag Player, Rigidbody with Use Gravity off, Constant Mover velocity (0, 0, 3), Contact ReporterSolid Wall at (-4, 1, 0), Scale (3, 2, 0.4), Contact Reporter, no Rigidbody| lane | you should see | in the Console |
|---|---|---|
| 1 | the ball stops at the wall | OnCollisionEnter on both |
| 2 | the ball sails through | OnTriggerEnter, then OnTriggerExit, on both |
Worked example
Two static cubes, overlapping, with reporters on both. This is the part people skip.
// both cubes: BoxCollider, no Rigidbody, overlapping by 0.8 units
// after 4 seconds the report prints:
// Static X | solid collider + no Rigidbody | collision enters 0 | trigger enters 0
// Static Y | solid collider + no Rigidbody | collision enters 0 | trigger enters 0| object | collider | Rigidbody | callbacks |
|---|---|---|---|
| Static X | Box, solid | none | 0 |
| Static Y | Box, solid | none | 0 |
| (add a Rigidbody to Y) | Box, solid | dynamic | both wake up - and Y falls |
The third row is the experiment
Why: Add the Rigidbody, watch the callbacks appear, then remove it again. Seeing silence turn into signal from one checkbox is what makes the rule stick.
Hypothesis
You tick Is Trigger on the solid wall in lane 1 and press Play.
Predict first
What changes?
Correct: The ball passes through, and both objects get OnTrigger* instead.
Why: One checkbox changes both halves at once: what is reported, and whether anything is stopped. That coupling is the thing to hold on to - you cannot have a trigger that blocks, or a solid collider that reports trigger callbacks.
Real world
Discussion prompt
A player walks over a coin and nothing happens. List the checks you would make, in order, and what each one rules out.
Answer:
OnTriggerEnter(Collider other) - a Collision parameter is never called.other.name before the check; a player tagged Untagged fails silently.Every one of those is a two-second check, and they are ordered by how often they are the answer. Working the list beats reading the code again.
Explain it
Discussion prompt
A teammate's trigger is not firing. Explain the two switches in two sentences, without using the word 'matrix'.
Answer:
Model answer: 'Something in the pair has to have a Rigidbody - usually whatever is moving - because two objects that are both just scenery are never even tested against each other.'
'And if either collider has Is Trigger ticked you get the OnTrigger callbacks and nothing gets blocked, so check which of the two methods you wrote.'
Concept
Eight experiments from the lab notes.
| change | what happens |
|---|---|
Rename OnTriggerEnter to OnCollisionEnter on the zone | it never fires - a trigger only sends trigger callbacks |
| Remove the Rigidbody from Ball A | it stops moving AND stops reporting |
| Tick Is Trigger on the solid wall | the ball passes through and the callbacks change family |
Use gameObject.tag == "Player" | works, allocates, and fails silently on a typo |
CompareTag("Enemy") with no Enemy tag defined | throws, naming the tag - which is the point |
| Add a Rigidbody to Static Y | the silent pair wakes up, and Y falls over |
| Make Ball A kinematic | it drives through the wall in silence |
| Tick Log Stay on the wall | OnCollisionStay fires every physics step while resting |
Elimination
A damage zone works for the player but not for enemies, and both have colliders.
Eliminate the wrong options
What should you check first?
Survives elimination: A
Why: The difference between the two cases is what the objects are, not what the code says - so look at the objects. A player usually has a Rigidbody or a Character Controller; enemies moved by a script with neither are two static colliders as far as the zone is concerned, and the pair is never tested. Choice D is the right second check, and it is the one line of code worth reading.
Pattern
Five questions, in this order. They diagnose any 'nothing happens' in under a minute.
OnTrigger* and nothing is blocked; solid means OnCollision* and things are stopped.OnCollisionEnter(Collision) and OnTriggerEnter(Collider). A mismatch is silent.And when identifying what you hit: CompareTag over tag ==, and TryGetComponent over both when you care about a capability rather than a label.
Check
Check your understanding
A ball with a Rigidbody rolls into a box whose collider has Is Trigger ticked. Which callback fires?
Answer: B
Why: A trigger anywhere in the pair makes the whole interaction a trigger interaction: OnTrigger* fires on both objects and nothing is stopped. There is no collision to report because nothing was resolved.
Check
Check your understanding
Two cubes with Box Colliders overlap. Neither has a Rigidbody. Both have scripts implementing every callback. What fires?
Answer: C
Why: Two static colliders are never tested against each other. The engine treats static geometry as a fixed structure to test MOVING things against, and pairs within it are skipped entirely - a performance decision, since a level can hold thousands of static colliders.
Socratic
Discussion prompt
Why does Unity deliver the callback to BOTH objects rather than only to the one with the Rigidbody?
Answer:
Because which object 'owns' the interaction is a design decision, not a physics fact. A door should decide whether it opens; a bullet should decide what damage it deals; a damage zone should decide how much it hurts.
If only the moving object were told, every piece of scenery would need the mover to know about it - so a coin would need the player's script to list every kind of pickup. Telling both sides lets the logic live wherever it belongs.
Check
Check your understanding
Inside OnCollisionEnter(Collision collision), how do you check whether you hit the player?
Answer: B
Why: Reach the GameObject through the Collision, then ask it with CompareTag - which allocates nothing and throws if the tag is not defined, so a typo is a loud error rather than a silent false.
Estimation
A player rests on the ground for 10 seconds, with OnCollisionStay implemented on both.
Predict first
Roughly how many times does that callback run?
Correct: About 500 - 50 physics steps a second for 10 seconds.
Why: And that is one pair. A character touching the ground and two walls triples it, and every object in the scene is doing the same. This is why Stay callbacks must be cheap, and why damage-over-time is written as damage times Time.fixedDeltaTime rather than a fixed amount per call.
Missing information
Discussion prompt
'Make the player take damage from the spikes.' What do you need to establish before writing it?
Answer:
The third bullet is the one that catches people: a Character Controller is not a Rigidbody, and half the rules in this deck change for it.
Concept
This is the eighth lesson in the sequence the diagnostic recommended. Together they cover the whole loop: objects, prefabs, physics, lifecycle, the API calls, transforms, vectors and contact.
| still on the report | why it was not a lesson |
|---|---|
| Audio, build settings, profiling | Fragile at 2/3, but not on the recommended lesson order - worth a short ninth session |
| Canvas and UI | tested fine, and it is its own subject |
| Time.deltaTime | tested fine, and it appeared in lessons 3, 6 and 7 anyway |
Analogy
Match the pairs
Match each Unity idea to a familiar cousin.
Why: The hit-area analogy is the one worth keeping: a button's clickable region is not its picture, and getting them out of step produces exactly the Unity bug where an object looks like it should be hit and is not. Same idea, same failure mode.
Connect it up
Draw it
Draw a 2x3 grid: columns are 'solid, no Rigidbody' and 'trigger, no Rigidbody'; rows are 'solid + dynamic', 'solid, no Rigidbody' and 'kinematic'. Fill each cell with what fires, and circle the cell that catches people.
Exit ticket
Predict first
Which is still shakiest?
Correct: Whichever you picked - the lab's summary table shows all four in one Play session.
Why: You scored 2 of 3 here with low confidence on both correct answers, which is the profile of a rule half-remembered rather than derived. Naming the shakiest one and running its experiment converts it, and this is the last lesson in the recommended sequence - so it is worth being solid before the next session.
Recap
You can predict, from two objects' Inspectors alone, whether anything will happen when they touch.
OnTrigger* and nothing is blockedOnCollisionEnter(Collision), OnTriggerEnter(Collider) - a mismatch is silentCompareTag, or better, with TryGetComponent for a capabilityLab: unity-labs/Assets/NaruhodoLabs/Lesson08_Collisions/SETUP.md. Run the three lanes, then go back through the eight recap slides and check you can say each lesson's one sentence without looking.
Want this taught 1-on-1? Alexander tutors Unity Game Engine — $55/session, free consultation.