◀ 2.12.22.3 ▶

2.2 — Spikes: Physics, Damage & Death

You built a coin you can walk through. Now build a spike you cannot: one checkbox, and it breaks your game — because it reveals something you have been carrying since 1.4. Your character is a ghost.

In this lesson you will be able to answer“how do I take damage when I touch a spike?”Guided build
Break It Down~10 minGuided build

Take damage when I touch a spike — for a character that is currently a ghost: the physics engine knows it is standing there and has never once been asked to move it.

First, before any code: cut that into parts small enough that you could look each one up on its own.

  1. Give the spike a shape that pushes backA Collider2D on the Spikes prefab, sized to the art, with Is Trigger left off, on the object carrying the Spikes tag.Read: the OnCollisionEnter2D(Collision2D c) row in the quick reference, and the Is Trigger setting you used on the coin in 2.1.
  2. write this partRead: the rb.linearVelocity = v row and the Body Type warning in the quick reference, and the movement table earlier in this lesson.
  3. Confirm the thing you hit was a spikeOnCollisionEnter2D(Collision2D collision) fires when solid colliders meet, and its parameter is a Collision2D — a container, not a collider — which is why the tag is asked of collision.gameObject rather than of collision itself.Read: the OnCollisionEnter2D(Collision2D c) and other.CompareTag("Coin") rows in the quick reference.
  4. Take the damage, and stop at the floorHealth is a class-body field, and one method changes it by a signed amount so the same method can heal later. The subtraction is guarded: if health is already zero, return before touching the number, so it can never run to -1, -2, -3 and stop meaning anything.Read: the if (health <= 0) { ... } else { ... } row in the quick reference, and the coin count field you declared in 2.1.

Note which part is the gap and which three are last lesson's coin pickup wearing a different tag. What is genuinely new is who moves the Player — and until that changes, a solid spike is a picture you can stand inside.

The test: does the part name the thing that does the work? "Take damage" is an effect, not a part. "Hand the movement to the physics engine" is a part — and it is the only one here that changes what your character is allowed to do.

Your turn

Your coin count only ever goes up, so it needs no rule at all. Health only goes down, and it has a floor. Which part above keeps health inside the range where the number still means something — and what would the Console print on a second spike without it?

In the Editor ~20 min
  1. Hierarchy → select the Overworld scene's contents, then drag two or three Assets/Prefabs/Spikes instances out of the Project window and into the Scene view. The scene ships with no spikes placed at all.
  2. Project window → Assets/Prefabs/Spikes. Select the prefab asset, then Inspector → Add Component → Box Collider 2D. Drag the size handles until the green outline matches the art.
  3. Inspector → Box Collider 2D → Is Trigger ☐ OFF. Leave it off: this is the one setting that separates a spike from a coin, because a push is the only thing that can stop you.
  4. Project window → Assets/Prefabs/Player, then Inspector → Rigidbody 2D → Body Type = Dynamic. It is already Dynamic from 2.1. Leave it.
  5. Inspector → Rigidbody 2D → Constraints → Freeze Rotation Z ☑ ON. In 2.1 you left Constraints alone because nothing in the world was solid to you. An off-centre push imparts torque, and without this your character tumbles like a dropped coin the first time it hits a wall at an angle.
    Inspector showing PlayerInspectorPlayerSprite RendererCircle Collider 2DRadius0.5Rigidbody 2DBody TypeDynamicMass1Linear Drag0Angular Drag0.05Gravity Scale0InterpolateInterpolate5ConstraintsFreeze PositionXYFreeze RotationZAdd Component
    The Rigidbody 2D from 2.1, scrolled down. Constraints is a foldout at the bottom — click it open and Freeze Rotation is the second row under it, with a single Z box because a 2D body cannot rotate in the other two axes.
  6. Press Play and walk into your spike. You walk straight through it. That is the bug this whole lesson exists to fix.
In the Code: Choose Your Movement Method ~15 min

You read this table in 1.5 as a prediction. Now it is a decision you live with for the rest of the year — so read the last two columns, and the warning underneath, because someone already fell into it for you.

TechniqueHow it worksStops at walls?What it requires
transform.Translate(vector) Offsets coordinates directly. Never consults physics. No Nothing. This is what you have now.
Rigidbody2D.linearVelocity = vector States how fast the body is travelling; the physics engine performs the motion. Yes A Rigidbody2D with Body Type Dynamic. Movement is in units per second, so no Time.deltaTime.
Rigidbody2D.MovePosition(position) Sweeps the body to a target position and tests the path as it goes. Yes — but only on a Dynamic body A Dynamic Rigidbody2D — the opposite of what the docs recommend.
Rigidbody2D.AddForce(vector) Applies a physical push; mass, drag and inertia all apply. Yes A Dynamic Rigidbody2D with a mass you will have to tune.
The documentation recommends the one combination that does not work. Unity's page for MovePosition(position) says it "is intended for use with kinematic rigidbodies" — and the same page admits that on a kinematic body "collisions won't affect the rigidbody itself". The manual goes further: a Kinematic body "only collides with Dynamic Rigidbody 2Ds." On a Kinematic body you arrive wherever you asked and nothing stops you on the way. The rule that survives all of it: for anything to stop you, your body must be Dynamic — which is why this course uses linearVelocity.
Why your coins still work either way. Triggers are the exception: a collider with Is Trigger ticked reports the overlap to any body type, including Kinematic. A coin picks up either way; a wall and a cave door only notice a Dynamic you.

Choose, Then Justify

  1. Your character is a ghost. Which row fixes that with the smallest change to the code you have already written?
  2. This game is about exploring a world and being shot at by turrets. Do you want your movement to carry momentum? Which row gives you momentum whether you want it or not?
  3. Only one column in that table has the same value in every row that works. What is it, and why is that column the real decision rather than the technique you picked?
In the CodeGuided build

The question this answers“how do I stop my character walking through walls?”

You are editing Assets/Scripts/PlayerController.cs. The shape of your file stays exactly as it is: fields in the class body, input read in Update(). Only the lines that change what moves the character are yours to write.

This build leaves 1 line for you to write. Everything around it is given, so you always know the shape of what you are producing.

Step 1Declare a reference to the body

You gave the Player a Rigidbody2D in 2.1. To hand it instructions you need a reference to it.

// PlayerController.cs — class body, above Start(), with your other fields.
    private Rigidbody2D body;
  1. 2A field to hold the Player's Rigidbody2D component. private and no [SerializeField], because nothing outside this script needs to see it.Class body, not inside a method — the same rule from 1.3. This reference has to still be here on the next frame, so it belongs to the object.

Step 2Look it up once, in Start()

GetComponent<T>() is a search. Assign it once in Start() and remember the answer.

// PlayerController.cs — inside Start(), with your 1.6 lookups.
        body = GetComponent<Rigidbody2D>();
  1. 2Find the Rigidbody2D sitting on this same GameObject and store it in body. GetComponent<T>() looks a component up on the object this script is attached to.Once, in Start() — never in Update(). Calling it every frame is sixty searches a second for an answer that never changes — the same "decide once, remember it" instinct as declaring inputX in the class body.

Step 3Replace one line in Update()

Everything above the gap is the vector you built in 1.4, unchanged. One line at the bottom of Update() is different.

// PlayerController.cs — inside Update().
        inputX = Input.GetAxis("Horizontal");
        inputY = Input.GetAxis("Vertical");

        Vector2 direction = new Vector2(inputX, inputY);
        direction = direction.normalized;

        body.________________ = direction * moveSpeed;
  1. 2Unchanged from 1.5.
  2. 5The two axis values packed into a vector. This is a direction: it says which way, not how far.
  3. 6Unchanged from 1.5 — keeps diagonal movement from being faster.
  4. Your turn8Hand the body a velocity instead of moving the Transform yourself. This one line replaces transform.Translate(movement), and it is the whole fix.The property on body that says how fast it is travelling. In Unity 6 it is no longer called velocity.Two things changed at once, and both matter. First, you are asking the physics engine to do the moving — so it knows where you are going and can stop you. Second, there is no Time.deltaTime: velocity is already measured per second, so the engine applies the time for you. Multiplying by Time.deltaTime here is the most common mistake on this line.
Check your work: the finished filetry every gap first — then open this to compare

Compare against your own file rather than copying it. The gap is the linearVelocity line: there is no deltaTime because velocity is per second, and the frozen rotation exists because solid collisions push.

// ------------------------------------------------------------------
//  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 health = 3;                     // <- new in 2.2
    [SerializeField] private int coins;                          // from 2.1

    private Rigidbody2D body;                                    // <- new in 2.2
    private float inputX;
    private float inputY;
    private Animator animator;                                   // from 1.6
    private SpriteRenderer spriteRenderer;                       // from 1.6

    // ---- methods you own --------------------------------------------------
    void Start()
    {
        body = GetComponent<Rigidbody2D>();                      // <- new in 2.2
        animator = GetComponentInChildren<Animator>();
        spriteRenderer = GetComponentInChildren<SpriteRenderer>();
    }

    void Update()
    {
        inputX = Input.GetAxis("Horizontal");
        inputY = Input.GetAxis("Vertical");

        Vector2 direction = new Vector2(inputX, inputY);
        direction = direction.normalized;

        // No Time.deltaTime: velocity is per second, not per frame.
        body.linearVelocity = direction * moveSpeed;
    }

    void UpdateAnimation()
    {
        // unchanged from 1.6
    }

    // ---- callbacks: Unity calls these, you never do -----------------------
    private void OnCollisionEnter2D(Collision2D collision)
    {
        // Collision2D, not Collider2D — so the tag is asked of .gameObject.
        if (collision.gameObject.CompareTag("Spikes"))
        {
            TakeDamage(1);
        }
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Coin"))
        {
            coins = coins + 1;
            Debug.Log("Coins collected: " + coins);
            Destroy(other.gameObject);
        }
    }

    private void TakeDamage(int amount)
    {
        if (health <= 0)
        {
            return;
        }

        health = health - amount;
        Debug.Log("Health: " + health);

        if (health <= 0)
        {
            Debug.Log("Player died.");
        }
    }
}
In the Code: Health, Damage & Death ~20 min

This one is on you, in the same shape as the walkthrough above — no gap-by-gap guide, because every piece of it you have built once. A field in the class body is 2.1. A method that changes a value is 1.3. A callback that checks a tag is 2.1. Specification:

Health.
  • A field on PlayerController, beside coins — what type, what starting value, and how is it exposed as a tuning knob?
  • One method that changes health by a signed amount, so a later heal is the same call with a different sign. Why one method rather than separate heal and damage methods?
  • Which callback holds the spike check, and how do you identify the spike from inside it — given the parameter is a Collision2D and not a collider?
  • What condition means "already dead", and what should the method do at that exact moment?
  • After subtracting health, what do you print so a player can watch the number change with no UI at all?

A spike is solid, so it stops you. "Stopping you" and "hurting you" are two separate decisions, and there are three honest ways to build the hazard:

ApproachWhat the player feelsWhat firesThe risk
Solid spike You stop against it. Damage once per touch. OnCollisionEnter2D(Collision2D collision) You can get wedged against it, and sliding along it may not register at all.
Trigger spike You walk through it. Damage once per entry. OnTriggerEnter2D(Collider2D other) Standing inside it deals no further damage — usually wrong for a hazard.
Trigger spike + cooldown Repeated damage while you stand in it. OnTriggerStay2D(Collider2D other) It fires every frame. Without a cooldown you lose all your health instantly.

Death has more than one defensible design. Pick one and defend it: (a) reload the current scene with SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex) — everything resets, coin count included; (b) reload a scene by name — which scene, and does it contain a Player?; (c) do not reload at all — reset health, move the player back to the start, keep the coins.

Your Decision Record

  1. Which Rigidbody 2D settings did you have to change, and what did you see when one of them was wrong?
  2. What evidence from your own build proves your choice works — what did you run into, and what happened?

Checkpoint — before moving on, you must be able to:

Reference Docs

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

Next: 2.3 — Healing Pickups & the Max-Health Cap, where a value with a ceiling needs a rule, not just arithmetic.

0/6 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.