2.1 — Coins: Triggers, Tags & Your First Stat
Your character can walk, but it cannot touch anything. Walk straight over the coin and nothing happens: nothing in this entire project is built to notice a collision. Today you make the first object in the game that reacts to the player, and in doing so you switch the physics engine on for good.
Make a coin disappear when I touch it — "how do I detect a collision, and make something happen?" As one problem it is not even startable, because at this moment nothing in the whole project is built to notice that two objects touched.
First, before any code: cut that into parts small enough that you could look each one up on its own.
- Give the coin a shape the engine can testA Circle Collider 2D on the prefab root, its radius dragged until the green outline covers the art, with
Is Triggerticked.Read: theCollider2Drow in the quick reference, and the Is Trigger checkbox in the Inspector. - Give the Player a shape and a bodyA Circle Collider 2D and a Rigidbody 2D (Body Type Dynamic, Gravity Scale 0), both on the Player root — not the Sprite child, because a tag is asked of whichever object owns the collider.Read: the Body Type warning in the quick reference — it is the first thing to check when nothing happens.
- Confirm the thing that triggered was a coin
OnTriggerEnter2D(Collider2D other)runs on the first frame two shapes overlap; theother.CompareTag("Coin")check is what turns "something touched me" into "a coin touched me".Read: theOnTriggerEnter2D(Collider2D other)andother.CompareTag("Coin")rows in the quick reference. - Change the countInside the check:
coins = coins + 1;, against a[SerializeField] private intdeclared in the class body — the declare-versus-assign split from 1.3.Read: the[SerializeField] private float speed;row in the quick reference, and the declare-vs-assign rule you met in 1.3. - Remove the coin from the world
Destroy(other.gameObject):otheris a Collider2D, so.gameObjectwalks up to the whole coin.Read: theDestroy(gameObject)row in the quick reference, and the pickup pattern underneath it.
The last three parts are the pickup pattern in miniature — a check, a change, a destroy — and you will write them in that order all year. The first two are why the split has to reach into the Inspector: until they are done, the method never runs at all.
The test: when the coin does nothing, is there one part above you can point at and test on its own? A silent failure with no error in the Console is a physics problem, not a script problem.
Your turn
In one sentence: what would the Player do on the first frame it walked into a wall if that tag check were missing?
Do this first. Two objects have to be able to notice each other in the Inspector before a single line of code can run.
-
Project window →
Assets/Prefabs/Coin. Select the prefab asset, not an instance sitting in the scene. Anything you change here applies to every coin you ever place. - Inspector → Add Component → Circle Collider 2D. That is the coin's shape. Drag Radius until the green outline covers the art.
-
Inspector → Circle Collider 2D → Is Trigger ☑ ON. This changes what an
overlap means: a solid collider shoves objects apart, a trigger does not push
at all — it only reports that the overlap happened.
The coin's Inspector, with the prefab selected. The box you want is the one just under the component name — Is Trigger, notRadius, which is at the bottom. -
Project window →
Assets/Prefabs/Player. Select the Player root in the prefab, not the Sprite child — the root is the object taggedPlayer. - Inspector → Add Component → Circle Collider 2D (if the Player does not already have one). Drag Radius until the outline fits the character.
-
Inspector → Add Component → Rigidbody 2D (if it has not been added).
This is the body, and it is the one everyone forgets. Then set:
Body Type = Dynamic, Gravity Scale = 0,
Interpolate = Interpolate. Your world is seen from directly above —
there is no "down" for anything to fall toward — and Gravity Scale defaults to 1, which
slides your character off the bottom of the screen the moment you press Play.
The Player's Inspector. If the third component here is missing from yours, Add Component at the bottom of the pane is where it comes from — and Gravity Scalesits belowAngular Drag, not next toBody Type.
Socratic: What Is a Collider For?
- A Transform stores position, rotation and scale. What does a Collider2D store that a Transform has no way to express?
- The physics engine runs on its own schedule, not inside your
Update(). Why would Unity be built that way, instead of letting your script test for overlaps once a frame? - A trigger pushes nothing — it only reports. Which two other game behaviours would want a report-only overlap like that?
The question this answers“how do I make a coin disappear when I touch it?”
You are editing Assets/Scripts/PlayerController.cs. The editor work above is what lets the two objects notice each other. This is the script half: two additions to PlayerController.cs, each anchored by a comment telling you where in the file it goes.
Step 1Declare the field with your other fields
Every field in this file lives in one place: the class body, above Start(). The coin count goes there with the rest of them.
// PlayerController.cs — class body, above Start(), with your other fields.
[SerializeField] private int coins;- 2How many coins you have collected. An
int(whole number), and[SerializeField] privateso it appears in the Inspector but stays this script's business.Declared in the class body, never inside the method that changes it — the same rule from 1.3. This number has to still be here on the next frame, so it belongs to the object. Put it beside the method instead and every touch would start the count over at zero.
Step 2Write the callback below your own methods
Unity calls this one; you never do. It goes at the bottom of the file, below Update(), where the engine-called methods belong.
// PlayerController.cs — below Update(). Unity calls this; you never do.
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Coin"))
{
coins = coins + 1;
Debug.Log("Coins collected: " + coins);
Destroy(other.gameObject);
}
}- 2Unity calls this on the first frame two shapes overlap. The parameter is the other object's collider — the one that ran into you.
- 4Check what you touched before doing anything.
CompareTag(String tag)asks the engine directly; comparing tag strings with==builds a new string every call and can silently miss.Spikes, cave entrances and healing pickups all land in this same method before the unit is over. Without this check, the Player would vanish the first time it walked into anything carrying a collider. - 6Add one to the running total. No type in front —
coinsalready exists, so this assigns to it. Writingint coins = coins + 1;here would make a brand-new local that dies at the end of the frame and leave the real total frozen at zero. - 7Print the new total. With no UI in this game yet, the Console is the only way to watch the number go up.
- 8Removes the coin.
otheris a Collider2D, not a GameObject —.gameObjectwalks up from the collider to the whole coin.other.gameObject, notgameObject. PlaingameObjectdestroys the Player. Get it backwards and the game is instantly unplayable — cause it on purpose once, just to see it.
Check your work: the finished filebuild it as you read — then open this to check every line
Compare against your own file. The two things to check hardest: that coins sits in the class body with your other fields rather than beside the method, and that the destroy says other.gameObject.
// ------------------------------------------------------------------
// REFERENCE FILE — Assets/Scripts/PlayerController.cs
// Written to be read, not pasted. Build it with the walkthrough
// above, one step at a time, then open this and compare.
// If it is sitting in your project unchanged, that is the thing
// to go and do now — the walkthrough is still above you.
// ------------------------------------------------------------------
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// ---- class body: every field lives up here, above Start() --------------
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private int coins; // <- new in 2.1
private float inputX;
private float inputY;
private Animator animator; // from 1.6
private SpriteRenderer spriteRenderer; // from 1.6
// ---- methods you own: called from Update() ----------------------------
void Start()
{
animator = GetComponentInChildren<Animator>();
spriteRenderer = GetComponentInChildren<SpriteRenderer>();
}
void Update()
{
// your 1.5 movement and your 1.6 UpdateAnimation() call, unchanged
}
void UpdateAnimation()
{
// unchanged from 1.6
}
// ---- callbacks: Unity calls these, you never do -----------------------
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Coin"))
{
coins = coins + 1;
Debug.Log("Coins collected: " + coins);
Destroy(other.gameObject);
}
}
}
You just made a real design decision without being told to: the coin count lives on
PlayerController. Here are the three honest places a number like that can
live, and what each one costs.
| Where the number lives | What it looks like | What it costs |
|---|---|---|
A field on PlayerController |
[SerializeField] private int coins; |
Anything else that wants the number must find the player first. This is what you built, and it is the right call today — nothing else touches coins yet. |
A separate PlayerStats component |
Numbers live in a component whose only job is numbers. | One more component to cache and keep in sync — worth it once movement and stats start fighting over the same file. |
A global singleton StatManager |
Any script, anywhere, reads the numbers from one shared object. | Everything can reach it, so nothing protects it — and a stat that has to survive a scene change is a Unit 5 problem, not a Unit 2 one. |
Where Does a Number Belong?
- Which of the three did you just build, and which line of your own code proves it?
- Open
Assets/Scripts/StatManager.csand read it. If you filled it in as a singleton today, would your coin count survive a scene change? Would that be a feature or a problem right now? - The coin disappears when collected, but "how many coins I have" is not a property of the coin. So where does that number really belong — the object that vanishes, or the object that persists?
Checkpoint — before moving on, you must be able to:
- State the rule — a collider on both objects, a body on at least one — and say which object you gave the body to, and why
- Find every collider and Rigidbody 2D you added, in the pane you added it in, and explain trigger versus solid
- Explain what the parameter in
OnTriggerEnter2D(Collider2D other)refers to, and whyCompareTag(String tag)beats comparing tag strings - Point at your own file and say why
coinsis declared in the class body whilecoins = coins + 1;is not - Explain what happened at the cave entrance, and why it is not a bug
- Commit and push
Reference Docs
Need a deeper explanation? These pages cover the same concepts:
- Unit 2: Stats & Pickups — colliders, triggers, and health in depth
- Triggers vs Collisions — the complete reference for this physics family, used all year
- Unity + C# Quick Reference — Collider2D and Rigidbody2D APIs
- How do I…? index — find this lesson again by the question you were asking
Next: 2.2 — Spikes: Physics, Damage & Death, where you discover that the movement you wrote ignores all of this.
Saved on this device only — the Dashboard tracks your progress.