◀ 1.62.12.2 ▶

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.

In this lesson you will be able to answer“how do I detect a collision, and make something happen?”Full walkthrough
Break It Down~10 minFull walkthrough

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.

  1. 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 Trigger ticked.Read: the Collider2D row in the quick reference, and the Is Trigger checkbox in the Inspector.
  2. 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.
  3. Confirm the thing that triggered was a coinOnTriggerEnter2D(Collider2D other) runs on the first frame two shapes overlap; the other.CompareTag("Coin") check is what turns "something touched me" into "a coin touched me".Read: the OnTriggerEnter2D(Collider2D other) and other.CompareTag("Coin") rows in the quick reference.
  4. Change the countInside the check: coins = coins + 1;, against a [SerializeField] private int declared 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.
  5. Remove the coin from the worldDestroy(other.gameObject): other is a Collider2D, so .gameObject walks up to the whole coin.Read: the Destroy(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?

In the Editor ~25 min

Do this first. Two objects have to be able to notice each other in the Inspector before a single line of code can run.

  1. 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.
  2. Inspector → Add Component → Circle Collider 2D. That is the coin's shape. Drag Radius until the green outline covers the art.
  3. 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.
    Inspector showing CoinInspectorCoinSprite RendererCircle Collider 2D3Is TriggerUsed By EffectorOffsetX 0 Y 0Radius0.32Add Component
    The coin's Inspector, with the prefab selected. The box you want is the one just under the component name — Is Trigger, not Radius, which is at the bottom.
  4. Project window → Assets/Prefabs/Player. Select the Player root in the prefab, not the Sprite child — the root is the object tagged Player.
  5. Inspector → Add Component → Circle Collider 2D (if the Player does not already have one). Drag Radius until the outline fits the character.
  6. 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.
    Inspector showing PlayerInspectorPlayerSprite RendererCircle Collider 2DRadius0.5Rigidbody 2D6Body TypeDynamicMass1Linear Drag0Angular Drag0.05Gravity Scale0InterpolateInterpolateAdd Component
    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 Scale sits below Angular Drag, not next to Body Type.
Press Play, then walk into a cave entrance. The scene will change. Nothing is broken — you switched the physics engine on. The cave entrances carry solid colliders wired to a scene-switching script that has been sitting dormant precisely because your Player had no collider to trip it. Write down what happened. Do not fix it and do not delete the entrances: Unit 5 builds the cave properly, and this is the first thread of it.

Socratic: What Is a Collider For?

  1. A Transform stores position, rotation and scale. What does a Collider2D store that a Transform has no way to express?
  2. 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?
  3. A trigger pushes nothing — it only reports. Which two other game behaviours would want a report-only overlap like that?
In the CodeFull walkthrough

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;
  1. 2How many coins you have collected. An int (whole number), and [SerializeField] private so 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);
        }
    }
  1. 2Unity calls this on the first frame two shapes overlap. The parameter is the other object's collider — the one that ran into you.
  2. 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.
  3. 6Add one to the running total. No type in front — coins already exists, so this assigns to it. Writing int 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.
  4. 7Print the new total. With no UI in this game yet, the Console is the only way to watch the number go up.
  5. 8Removes the coin. other is a Collider2D, not a GameObject — .gameObject walks up from the collider to the whole coin.other.gameObject, not gameObject. Plain gameObject destroys 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);
        }
    }
}
Check Yourself ~15 min

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 livesWhat it looks likeWhat 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?

  1. Which of the three did you just build, and which line of your own code proves it?
  2. Open Assets/Scripts/StatManager.cs and 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?
  3. 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:

Reference Docs

Need a deeper explanation? These pages cover the same concepts:

Next: 2.2 — Spikes: Physics, Damage & Death, where you discover that the movement you wrote ignores all of this.

0/7 answered

Answers save on this device as you type. At the end of class, click Download and upload the file to the Canvas assignment.

Saved on this device only — the Dashboard tracks your progress.