◀ 5.25.36.1 ▶

5.3 — Into the Cave: Scenes, Persistence & The Refactor

You break the door, you walk in — and everything you own breaks with it. Your coins are gone, your health is back to full, your axe has vanished. Nothing is wrong with your code: you left one scene and entered another. Today you meet the biggest refactor of the year.

In this lesson you will be able to answer“how do I keep my health when I change scenes?”Full walkthrough
Break It Down~10 minFull walkthrough

The numbers the player earned should still be there after they walk through a cave mouth. Right now they are not: coins, health, the axe — every one of them comes back at its starting value, and nothing in the project is built to survive a scene load at all.

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

  1. Notice that a scene load replaces your objects, not just your viewUnity destroys every GameObject in the old scene and builds the next one from its file, so the Player you were holding is gone and the Player that appears starts from whatever its prefab and its fields say. Nothing was lost and nothing was saved — the numbers were replaced.Read: the SceneManager.LoadScene(...) row of the quick reference.
  2. Sort the facts: this room, or this runPosition, facing and the animation controller are facts about this moment in this room and must reset — SceneSwitch.cs already restores them, which is why walking into a cave does not strand you. Coins, current health, the maximum, and the axe flag are facts about this run and must survive.Read: SceneSwitch.cs, the one file in the project that already saves and restores something across a load — you read it in 5.1.
  3. Build one owner that a scene load is not allowed to destroyA singleton: a public static field holding the one instance, an Awake() that claims it and calls DontDestroyOnLoad(gameObject) on the first load, and a second branch that destroys the copy every later load brings with it.Read: the DontDestroyOnLoad(gameObject) row and the Singleton pattern row of the quick reference.
  4. Move the numbers onto that owner — the ceiling with the value it protectsCoins, health, maxHealth and the axe flag leave PlayerController and become fields on StatManager, with the 2.3 rule written into the one method that can change health, right beside the field it bounds.Read: the damage method you wrote in 2.2 and the cap you added in 2.3.
  5. Repoint every caller, then put the numbers on screenEvery script that used to read a field on the player goes through StatManager.Instance instead, and a HUD in the scene the game begins in displays the values that have been going to the Console since 2.1.Read: the pickup branch you wrote in 2.1, and the Console readouts you have been watching all year.

The whole split falls along one line: what belongs to this room and what belongs to this run. Only one of the five parts has no precedent at all in this course — the object the scene loader is not allowed to destroy. Everything after it is moving house.

The test: could you look this part up on its own, right now, in one search? "Keep my stats between scenes" cannot be looked up. "Do not destroy this object when a new scene loads" can — it is one row of the quick reference.

Your turn

The project already contains one thing that survives a scene load: SceneSwitch.cs remembers which scene you came from, in a static field. Why is that field allowed to be static when your coins are not — what is different about the two things being remembered?

Today's design keeps your numbers alive across a scene load, and that is all it keeps them across. Quit the game and they are gone — is that a hole in the design, or the design itself?

In the Editor ~30 min
  1. Game view → press Play in the Overworld and play for a minute: collect a few coins, take a spike hit, pick up the axe. Then walk into one of the cave mouths. Everything you were carrying is gone. Notice what did follow you across — your position, your facing, your animation controller — and what did not.
  2. File → Build Profiles → Scene List (older tutorials call this Build Settings): three scenes, and their order is the load index — Start (index 0), Overworld (index 1), Platformer (index 2). Start (index 0) is what a built game loads first, and it is the scene that contains the StatManager object — and no Canvas at all.
  3. Hierarchy → Overworld → one of the two cave mouths → Inspector → Scene Switch → Scene: an int, holding 0 on one mouth and 2 on the other. That number is a position in that list, not a name: nothing checks that index 0 is still the scene you think it is.
  4. Project window → Assets/Scripts/StatManager.cs: open it. It reads public class StatManager : MonoBehaviour { } — an empty stub since Unit 2, and the object you are about to write.
  5. Once the walkthrough is done — Game view → press Play from Start (index 0), walk into a cave and come back: everything is still there. Then quit the game and start it again, and notice which of those two things is not true.
  6. Hierarchy (Start (index 0)) → GameObject → UI → Text - TextMeshPro: one menu item makes three objects — a Canvas, a Text (TMP) child, and an EventSystem, which the UI wants even when nothing on it is clickable.
  7. Inspector → Text (TMP) → RectTransform → anchor preset → top-left, with a small offset from the corner, so the numbers stay put when the window is resized — a centre-anchored label sits over the middle of the game.
    Inspector showing Text (TMP)InspectorText (TMP)RectTransform7Anchor PresetsText (TMP)
    RectTransform is the first component on anything the UI owns, and Anchor Presets is the grid of squares at the top of it: each square is one preset, and the light one is where this label is pinned now. Click the top-left square first — the boxes beside the grid are the offset, and until the anchor moves they are measuring from the wrong corner.
  8. Once HudController.cs exists — Inspector → Canvas → Add Component → Hud Controller, then Hud Controller → Stats Text → drag the Text (TMP) object in. An empty slot is a null reference that nothing complains about until the line that reads it runs.
In the Code ~45 min
APIWhat it gives youWhat it costs
public static StatManager Instance; A field that belongs to the class, not to an object — reachable with no reference and no search. It starts as null, and nothing claims it for you.
private void Awake() Unity's earliest hook: it runs once per object, and every Awake() in a scene finishes before the first Start() runs. Order between two objects' Awake() methods is still not promised.
DontDestroyOnLoad(gameObject) The object moves into a scene of its own that never unloads, so it survives every load from then on. It takes a whole GameObject, not a component.
Destroy(gameObject) Removes an object — how the newcomer copy takes itself out of the game. The removal is deferred to the end of the frame: a line running later in that same frame can still reach it.
SceneManager.LoadScene(int buildIndex) What SceneSwitch.cs calls at every cave mouth — the line destroying your stats. Hand it an int and it loads whatever scene sits at that position in the build list.
Architectural specification — this is a refactor, not a rewrite. Nothing your game does changes; what changes is where the numbers live. Answer these before you open the file.
  • The move. Which fields leave PlayerController, and which stay — and why does the one you keep belong to the player rather than to the run?
  • The owner. Which setup hook claims the slot, and what does its other branch do with a second copy?
  • The seam. Which single method may change health, and what ceiling does it enforce?
  • The screen. Where does the HUD live, and what would one that read from the player show after a scene change?

Move it, then compile: delete the fields from PlayerController first, and let the compiler list the callers you had forgotten.

Socratic: Three ways to make a number survive

All three are real Unity techniques. Compare what each one costs you:

ApproachHow it worksWhat it costs
A static field Belongs to the class — destroying the objects does not touch it. No lifecycle: you cannot reset it cleanly, and in the editor it survives between Play sessions, so testing lies to you.
A persistent singleton object One object registers as the instance and asks Unity not to destroy it. You must handle the second copy a scene load brings.
A ScriptableObject asset Lives in a project asset rather than in a scene, so no scene load can touch it. A new concept and one more asset to manage — and it survives Play sessions too, which is either the point or a nuisance.
  1. Your stats should reset when you start a new game, and survive a scene change. Which of the three can express both of those, and which one cannot express the first?
  2. Start (index 0) already contains its own StatManager object. If you walk back to it later, what will be there — and what happens if nothing stops it from registering itself a second time?
  3. Why can position not be handled this way? What is different about position that makes SceneSwitch.cs handle it instead?
In the CodeFull walkthrough

The question this answers“how do I keep my coins, my health and my axe when I change scenes?”

You are editing Assets/Scripts/StatManager.cs. This file has been public class StatManager : MonoBehaviour { } since Unit 2 — an empty stub, doing nothing at all. Today every line of it is written: the static slot, the Awake() that claims it, the persistence call, the duplicate guard, the four stats moved off the player, and the methods every other script will call. Step 4 is the part people forget.

Step 1Claim the slot on load, and refuse to be destroyed

This is the mechanism the whole lesson exists for: one shared field, and a setup method that decides whether this copy is the original or a newcomer.

    public static StatManager Instance;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
  1. 1The one field every other script will reach for. public so they can, static so it belongs to the class rather than to any object, and typed StatManager because what it will hold is this component. It starts as null — nothing has claimed it yet.Every word of that line is load-bearing. Drop static and the field becomes a fact about one object — and the object is exactly what a scene load destroys, so Instance would be empty again the moment you walked through a cave mouth. That is the whole trick of this lesson: a static field is stored against the class, and the scene loader has no idea the class exists.
  2. 3The one lifecycle method that runs before anything else when a scene loads: before any Start(), before any Update(). Unity finishes every Awake() in a scene before it runs the first Start(), which is precisely the guarantee a singleton needs — somebody has to be first, and everything else must be able to assume they already were.Not Start(). Unity promises no order between two different scripts' Start() methods, so a script whose Start() happened to run before the manager's would read Instance while it is still null and throw a NullReferenceException — pointing at itself, not at the manager. Move this method into Start() and the bug shows up only sometimes, which is the worst kind of bug there is.
  3. 5The first of two branches: is the slot still empty? On the very first scene load it always is, because nothing has run this code before.
  4. 7this is the component this method is running inside — this very StatManager. Claim the slot with it, so every later caller reaches this object and the numbers on it.
  5. 8The one call in Unity that changes what a scene load is allowed to do. gameObject is the whole object this component sits on — the StatManager object in the scene, not the script. Unity moves it into a scene of its own that never unloads, so it rides along through every load from now on.gameObject, not this. this is a component, and DontDestroyOnLoad(...) is defined for whole objects — and it has to be the whole object anyway, because a component cannot outlive the GameObject that carries it. Notice what is not happening here: nothing is being made permanent about the values. They are just sitting on an object a scene load is no longer allowed to delete.
  6. 10The second branch, and the one people delete because it looks like extra caution. It is not.
  7. 12You are the newcomer: an original is already alive, so this copy must go — Destroy(gameObject) takes the component, its fields, and the fresh coins = 0 it was about to claim the slot with.**This branch is genuinely necessary in this project, not a textbook exercise.** Start (index 0) contains its own StatManager object, and one of the Overworld's two cave mouths leads back to it — so a second copy is a scene you can actually walk into. Without this branch the newcomer overwrites Instance with its own default fields, and your coins and health are back at their starting values the instant the scene finishes loading. That is the bug you came here to fix, and this is the line that stops the fix from reintroducing it.
  8. 14Closes Awake(). One branch for the first scene and one for every scene after it — that pair is the singleton.

Step 2Move the four stats across

A move, not a redesign. Same names, same types, same starting values — the only thing that changes is the object they live on.

    [SerializeField] private int coins;
    [SerializeField] private int health = 3;
    [SerializeField] private int maxHealth = 3;
    [SerializeField] private bool hasAxe;

    // ...and nothing else. PlayerController keeps its speed, its body
    // and its input. The numbers are not the player's business.
  1. 1The coin count from 2.1, unchanged: an int, private, and still visible in the Inspector so you can watch it move while the game runs.Keep the name. Every script that mentions coins should keep reading the same word and change only who it is asked of. Rename it mid-refactor and you double the number of files you have to touch, and make "what did I break?" an unanswerable question.
  2. 2Current health, with whatever starting value your player shipped with.
  3. 3The ceiling from 2.3.It moves in the same step as health, and that is the whole point. A ceiling is a fact about the value it bounds. Leave maxHealth behind on the player while health lives here and the rule gets enforced across two objects that are free to disagree.
  4. 4The third kind of fact, from 5.1 — not a number at all, just true or false. It moves for exactly the same reason the numbers do: a cave mouth does not care what type a fact is.
  5. 6That is the entire move. PlayerController keeps what it is actually for — input, movement, the collision callbacks — and stops being the place the game keeps its numbers.The line between the two is worth saying out loud, because it is the difference between a refactor and a mess: the player is how you move through the world, and the manager is what the world has done to you.

Step 3Give the rest of the game a way in

Those fields are private, which is what you want — and it is why there has to be a public way to read and change them. These methods replace the ones that used to live on the player.

    public int GetCoins() { return coins; }
    public int GetHealth() { return health; }
    public int GetMaxHealth() { return maxHealth; }
    public bool HasAxe() { return hasAxe; }

    public void AddCoins(int amount)
    {
        coins = coins + amount;
    }

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

        health = health - amount;
        health = Mathf.Clamp(health, 0, maxHealth);
        Debug.Log("Health: " + health);

        if (health <= 0)
        {
            Debug.Log("Player died.");
        }
    }

    public void SetHasAxe(bool value)
    {
        hasAxe = value;
    }
  1. 1A public way to read the coin count without being able to write it. The field stays private, so the only route to that number is through the methods you are writing here.
  2. 2The same door for current health.
  3. 3And for the ceiling — the HUD shows health / maxHealth, so it needs both numbers rather than just the one.
  4. 4And the axe. A bool accessor reads a little differently out loud — HasAxe() — and that is a feature: a method named for the question it answers is easier to read at a call site than a field named for the thing it stores.
  5. 6Changing the count is a method, not a field assignment, and it takes an amount — so a coin is AddCoins(1) and a future five-coin bonus needs no new code at all.Why not just make coins public? Because then every script in the project could set it to anything, and the number would only ever be right in the one place somebody remembered to write it correctly. A method is a place where a rule can live.
  6. 8The line you wrote in 2.1, with the 1 replaced by the parameter.
  7. 11This is your 2.2 method, moved — same name, same int amount, same meaning: a signed amount, where a positive number hurts and a negative one heals.Same name and same signature on purpose. Every call site in your project reads TakeDamage(...) today, so keeping the name means this refactor changes who is called and nothing else. Rename it and you are editing every caller by hand, hoping you found them all.
  8. 13The guard you put at the top of this method in 2.2, carried across word for word: a player who is already at zero takes no more damage.This is your line, not mine. If your 2.2 rule refuses damage at zero some other way, yours is the one that stays — a refactor moves a rule, it does not improve it.
  9. 15Leave the method immediately, so an already-dead player cannot be pushed further.
  10. 18The subtraction from 2.2, unchanged. A positive amount takes health away; a negative one gives it back.
  11. 19The cap from 2.3, now sitting directly under the field it protects. Whichever tool you chose in 2.3 moves across here — if you wrote Mathf.Min(...), it works just as well.This line is why the ceiling had to travel with the value. The rule now lives inside the only method that can change health, on the only object that holds it — so no caller, today or next month, can put health above the maximum.
  12. 20Your Console readout, exactly as it was. Still worth keeping while you test — the HUD you build next will not replace it, it will just mean you no longer need it.
  13. 22The other half of your 2.2 rule: what happens when health reaches zero.
  14. 28The one setter. The axe pickup calls SetHasAxe(true) from 5.1's branch, and nothing else in the game should ever write that field.
  15. 30A bool parameter, assigned straight across. value is the name C# gives the thing that came in.

Step 4Repoint the callers — the part people forget

The file is finished and the game does not compile yet: every script that used to reach for a field on the player is reaching for something that is no longer there. Search your own code for coins, health, maxHealth and hasAxe, and change each one.

        if (collision.gameObject.CompareTag("Spikes"))
        {
            StatManager.Instance.TakeDamage(1);
        }

        if (other.CompareTag("Coin"))
        {
            StatManager.Instance.AddCoins(1);
            Destroy(other.gameObject);
        }

            StatManager.Instance.SetHasAxe(true);

        // Nothing else changes: movement, input, the tag checks and the
        // Destroy calls are all untouched. Only the receivers changed.

        // No null guard here: if there is no StatManager in the scene, that
  1. 3The same call with a new receiver. player.TakeDamage(1) becomes StatManager.Instance.TakeDamage(1), and the rule it runs is still one line away in the file you just wrote.And this caller never had anything to do with the player. Your projectile from 3.4 was handed a reference to the Player for one reason: to call this method. It does not need one now — StatManager.Instance.TakeDamage(damage); reaches the same rule from anywhere.
  2. 8One coin, through the method that counts them.
  3. 125.1's axe branch, with its receiver changed in exactly the same way.
  4. 14That sentence is the definition of a refactor. If you find yourself rewriting a check, renaming a method or "improving" a branch while you are in there, stop — you are rewriting, and now nothing you break is traceable to a single change.
  5. 17The honest answer to "should every caller check for a manager?" — no. Instance is null only when the scene you pressed Play in has no manager, which means you started the game from the Overworld instead of Start (index 0). A guard would hide a broken scene and make the Console silent about it. Fix the scene; write the guard only where code can genuinely run without a manager, which the HUD does next.This is a judgement call worth naming, because the instinct — from learning to write if (slot != null) before every Inspector reference in 4.1 — is to guard everywhere. Guarding everywhere converts a loud, traceable failure into a quiet, untraceable one.
Check your work: the finished filebuild it as you read — then open this to check every line

Compare against your own file rather than copying it. Three things have to match: the fields are gone from PlayerController rather than copied, the duplicate branch is really there — this project loads a second StatManager the moment you walk back into Start (index 0) — and your own damage and death rules came across from 2.2 and 2.3 exactly as you wrote them. The accessor names — Instance, GetCoins, GetHealth, GetMaxHealth, HasAxe, AddCoins, TakeDamage, SetHasAxe — are the ones the HUD calls.

// ------------------------------------------------------------------
//  REFERENCE FILE — Assets/Scripts/StatManager.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;

// StatManager.cs — the numbers the player earned, on the one object a scene
// load is not allowed to destroy.
public class StatManager : MonoBehaviour
{
    public static StatManager Instance;

    [SerializeField] private int coins;
    [SerializeField] private int health = 3;
    [SerializeField] private int maxHealth = 3;
    [SerializeField] private bool hasAxe;

    private void Awake()
    {
        // Somebody is already alive. This copy is the one that leaves — the
        // Start scene has its own manager object, and you can walk back to it.
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public int GetCoins() { return coins; }
    public int GetHealth() { return health; }
    public int GetMaxHealth() { return maxHealth; }
    public bool HasAxe() { return hasAxe; }

    public void AddCoins(int amount)
    {
        coins = coins + amount;
    }

    // One method, one signed amount: the spike calls it positive, the potion
    // calls it negative. Same name and signature as the one on the player, so
    // every existing caller changes its receiver and nothing else.
    public void TakeDamage(int amount)
    {
        health = health - amount;

        // The ceiling from 2.3, on the only object that holds the value it
        // protects. No caller can get above it.
        health = Mathf.Clamp(health, 0, maxHealth);
        Debug.Log("Health: " + health);

        // ...and YOUR death rule from 2.2 goes here: the "already at zero"
        // guard at the top of the method, and whatever should happen when
        // health reaches zero. Moved across, not rewritten.
    }

    public void SetHasAxe(bool value)
    {
        hasAxe = value;
    }
}

// Callers, in PlayerController.cs — the receivers changed, nothing else:
//   StatManager.Instance.TakeDamage(1);      <- the spike branch (2.2)
//   StatManager.Instance.AddCoins(1);        <- the coin branch (2.1)
//   StatManager.Instance.SetHasAxe(true);    <- the axe branch (5.1)
// ...and in the projectile from 3.4, which no longer needs a reference to the
// player at all: StatManager.Instance.TakeDamage(damage);
Build Lab: The HUD, and Where the Game Begins ~20 min

One decision is left, and it is yours: which scene should the game actually begin in? A Canvas is not persistent — it exists only in the scene that contains it.

ApproachWhat you doWhat it costs
Move the Overworld's Canvas into Start (index 0) Drag the Canvas and its Text (TMP) child across, then repair whatever breaks. Less setup, but you inherit the lever's third listener. It lives on the scene instance of CaveSwitch, not on the prefab: move the Canvas and that slot reads Missing, and the lever quietly stops doing one of its three jobs.
Leave it alone and build a fresh Canvas in Start (index 0) One menu item: GameObject ▸ UI ▸ Text - TextMeshPro. A second Canvas exists in the project, and the Overworld's still switches off when the lever fires — harmlessly, because nothing is drawn on it. You break nothing you did not write.

Either is defensible. This lesson builds a fresh one, because that path leaves the wiring you inherited exactly as you found it. Whichever you choose, write down your reason before you start: the checkpoint asks you for it.

In the CodeFull walkthrough

The question this answers“how do I put my coins and my health on the screen?”

You are editing Assets/Scripts/HudController.cs. A new script, and the Canvas you need for it is already standing in Start (index 0) — that was the editor work above. The script itself is a handful of lines: a slot for the text, a guard, and one string built from the values StatManager.Instance hands you. Every line is given.

Step 1Give the script a slot for the text

Two lines above the method: one using and one field.

using TMPro;

    [SerializeField] private TextMeshProUGUI statsText;
  1. 1TextMeshPro's types live in a namespace called TMPro, and the UI package that ships them does not put them in scope for you. Without this line the compiler cannot find the type on the next line, and reports an error that reads like a typo.
  2. 3The text object, wired from the Inspector. One thing here is new: the type is not GameObject or Transform, it is the component — because the component is what owns the text property you are about to write to.A slot for the component, not for the object. You have held component references since 2.2, but always by looking them up at runtime with GetComponent<Rigidbody2D>(). This one is dragged in by hand, which is only possible because TextMeshProUGUI is a component type the Inspector can hold.

Step 2Read the numbers from the manager, and write them into the text

One method, Update(), and it takes a position on the design question this lesson asks out loud: read every frame, and do not get clever about it.

    void Update()
    {
        if (StatManager.Instance == null)
        {
            return;
        }

        string statsLine = "Coins: " + StatManager.Instance.GetCoins()
                         + "   Health: " + StatManager.Instance.GetHealth()
                         + " / " + StatManager.Instance.GetMaxHealth();

        if (StatManager.Instance.HasAxe())
        {
            statsLine = statsLine + "   Axe: yes";
        }

        statsText.text = statsLine;
    }
  1. 1Every frame, like your movement and your enemy logic. A number can change at any moment — a coin is collected in a trigger callback, damage arrives from a projectile you never see coming — so a readout that is only correct sometimes is worse than no readout at all.The alternative is "only rewrite the text when a value changes", and here it costs more than it saves. You would keep the last three values as fields and compare them every frame — three extra fields, and three extra chances for one of them to go stale and freeze the label forever. Reading a value is a method call; the expensive part is building the string, which is exactly what you would be avoiding. At this scale the honest engineering answer is the simple one, and the moment it stops being true the fix is right here: remember the last values, and rebuild the string only when one of them moves.
  2. 3The guard — and here it genuinely earns its place. This script can run in a scene with no manager at all: the Overworld and the Platformer both contain a Player, and neither contains a StatManager. Press Play from either one and this method runs every frame with nothing to read.Without this line the Console fills with a NullReferenceException sixty times a second. That does not crash the game; it buries every other message you might need, which is worse. With the line, the label simply keeps whatever it last said. Neither is a fix — the real fix is that the game begins in Start (index 0) — but the guard turns a flood into a blank label, which is a failure you can see.
  3. 5Nothing to show, so show nothing. return leaves the method immediately — the same early-exit shape as the death rule in your 2.2 damage method.
  4. 8Build the line. The left-hand side is a local — a string that exists for this frame only, which is correct: it is thrown away and rebuilt next frame anyway, and a field would just be a place for a stale one to hide.Every value comes from StatManager.Instance, not from the player. The manager is the only object that still has the numbers, and the only one that survived the scene change — which is why the HUD can be trusted at all. Read them off the player and you get a fresh copy of the player's starting values in every new scene, which is the bug you spent the last activity fixing.
  5. 9The second number, appended to the same line. Adding two strings gives you one longer string.
  6. 10And the ceiling, so the label reads 2 / 3. Showing the maximum and not just the current health is a small design decision worth naming: a player can only judge "am I in trouble?" against what full looks like.
  7. 12The third fact from 5.1, added only when it is true — so the label does not spend screen space saying Axe: no for the whole game.
  8. 14Appending to the string you already built. Note the shape: assign back to the same name, because a string cannot be changed in place — this makes a new one and points the name at it.
  9. 17The write. statsText is the component you wired in the Inspector; .text is the property holding the characters on screen. Assign to it and the label changes on this frame.Nothing reads the text back, and nothing stores it. The label is a view of the numbers, not a copy of them — it holds no game state of its own. That is what makes it impossible for the HUD and the truth to disagree. The moment you cache a number in this script "to save time", you have built a second source of truth, and it will be the wrong one.
Check your work: the finished filebuild it as you read — then open this to check every line

Compare against your own file rather than copying it. Three things to check: the using TMPro; line is there, the string is built inside Update() rather than once in Start(), and every value is read from StatManager.Instance rather than from the player. The walkthrough builds that string in one expression and appends the axe line where this file assigns it in an if/else; both write the same characters to the same property. Your label text, your placement and what you choose to display are yours — the shape is the slot, the guard, and the rebuild in Update().

// ------------------------------------------------------------------
//  REFERENCE FILE — Assets/Scripts/HudController.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;
using TMPro;

// HudController.cs — the numbers, on screen. It owns no state at all: every
// value is read from the manager each frame and thrown away again.
public class HudController : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI statsText;

    void Update()
    {
        // No manager in this scene (you pressed Play in the Overworld)? There
        // is nothing to show and nothing to crash over.
        if (StatManager.Instance == null)
        {
            return;
        }

        int coins = StatManager.Instance.GetCoins();
        int health = StatManager.Instance.GetHealth();
        int maxHealth = StatManager.Instance.GetMaxHealth();

        string line;

        if (StatManager.Instance.HasAxe())
        {
            line = "Coins: " + coins + "   Health: " + health + " / " + maxHealth + "   Axe: yes";
        }
        else
        {
            line = "Coins: " + coins + "   Health: " + health + " / " + maxHealth;
        }

        statsText.text = line;

        // If reading every frame ever shows up as a cost, the fix is not to
        // stop reading. Remember the last values and rebuild the string only
        // when one of them changes.
    }
}

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

Reference Docs

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

Next: 6.1 — Gravity & Jump — Unit 6 begins, and now that you have one object holding all your state, it is finally safe to add physics that could kill you.

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