Summary

The GameManager is a single script — typically attached to one empty GameObject — that owns all shared game state: score, health, lives, and game-over status. Other scripts do not read or write this state directly; they call public methods on the GameManager (AddScore, TakeDamage, Heal). This concentration of state in one place makes bugs easier to find and fixes easier to apply.

In single-scene games, the GameManager is wired via Inspector references. In multi-scene games, it is extended with the singleton pattern so it persists across scene loads and can be accessed from anywhere without a wired reference.

The beginner version should be plain before it is clever: one GameManager object, private state, public methods, and other scripts calling those methods through Inspector references. The singleton version solves a later problem, not the first one.

Basic GameManager

State fields (private, Inspector-visible via [SerializeField]):

public class GameManager : MonoBehaviour
{
    [Header("Score")]
    [SerializeField] private int score = 0;
    [SerializeField] private int coinsCollected = 0;
 
    [Header("Health")]
    [SerializeField] private int health = 100;
    [SerializeField] private int maxHealth = 100;
 
    [Header("Lives")]
    [SerializeField] private int lives = 3;
 
    private bool isGameOver = false;

Public method interface:

    public void AddScore(int amount)
    {
        if (isGameOver || amount <= 0) return;
        score += amount;
        coinsCollected++;
        Debug.Log("[GameManager] Score: " + score);
    }
 
    public void TakeDamage(int amount)
    {
        if (isGameOver || amount <= 0) return;
        health = Mathf.Max(health - amount, 0);
        Debug.Log("[GameManager] Health: " + health + "/" + maxHealth);
        if (health <= 0) LoseLife();
    }
 
    public void Heal(int amount)
    {
        if (isGameOver || amount <= 0) return;
        health = Mathf.Min(health + amount, maxHealth);
        Debug.Log("[GameManager] Healed to: " + health + "/" + maxHealth);
    }

Private logic:

    private void LoseLife()
    {
        lives--;
        Debug.Log("[GameManager] Life lost. Lives remaining: " + lives);
 
        if (lives <= 0)
        {
            isGameOver = true;
            Debug.LogError("[GameManager] Game over. Final score: " + score);
            // SceneManager.LoadScene("GameOver") — see Scene management below
        }
        else
        {
            health = maxHealth;  // respawn with full health
            Debug.Log("[GameManager] Respawned. Health reset.");
        }
    }

Read-only accessors:

    public int  GetScore()          => score;
    public int  GetHealth()         => health;
    public int  GetLives()          => lives;
    public int  GetCoinsCollected() => coinsCollected;
    public bool IsGameOver()        => isGameOver;
}

Reading the responsibility split:

  1. The fields hold shared game state: score, health, lives and game-over status.
  2. Other scripts do not edit those fields. A coin calls AddScore; a hazard calls TakeDamage; a health pack calls Heal.
  3. Each public method checks whether the call is valid before changing state. if (isGameOver || amount <= 0) return; prevents late or nonsensical updates.
  4. Private helper logic such as LoseLife is hidden inside the GameManager because other scripts do not need to know how losing a life works.
  5. Accessors such as GetScore let UI or debugging code read state without being able to change it directly.

This is the course object communication chain: event object detects something, asks the GameManager to update state, then UI or feedback scripts can respond to the new state (course lab series, see source-csharp-unity-labs).


Singleton pattern

The singleton ensures only one GameManager instance exists at runtime, and allows any script to access it without a wired Inspector reference. Use this when the GameManager must persist across scene loads, or when many scripts across many scenes need to reach it.

public class GameManager : MonoBehaviour
{
    // Static property: belongs to the class, not any instance
    // Any script can call: GameManager.Instance.AddScore(10)
    public static GameManager Instance { get; private set; }
 
    private void Awake()
    {
        // If another instance already exists, destroy this one
        if (Instance != null && Instance != this)
        {
            Debug.Log("[GameManager] Duplicate detected — destroying self.");
            Destroy(gameObject);
            return;
        }
 
        Instance = this;
 
        // Persist this GameObject across scene loads
        DontDestroyOnLoad(gameObject);
        Debug.Log("[GameManager] Singleton initialised.");
    }
 
    // ... state fields and public methods as above
}

Calling the singleton from any script (no wired reference needed):

GameManager.Instance.AddScore(10);
int currentScore = GameManager.Instance.GetScore();

Scene management

using UnityEngine.SceneManagement;
 
public void RestartGame()
{
    // Reset state
    score = 0;
    lives = 3;
    health = maxHealth;
    isGameOver = false;
 
    // Reload the active scene by name
    SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
 
public void LoadScene(string sceneName)
{
    SceneManager.LoadScene(sceneName);
}

When to use the singleton

The singleton is a solution to a specific problem: state that must be shared across scenes, or that dozens of scripts in many locations need to reach without wiring. Do not use it as a default pattern for every manager object.

Use the singleton when:

  • The game has multiple scenes and score/lives must carry across them
  • Many unrelated scripts all need to call the same manager

Use a plain Inspector-wired reference when:

  • The game is single-scene
  • Only a handful of scripts communicate with the manager

Over-centralising state in a singleton encourages scripts to become passive — they stop managing their own responsibilities and delegate everything upward. Introduce the singleton only once a concrete need for it arises.


Singleton vs events

“Should I use a singleton GameManager or an event system?” is a common question, but the two answer different questions, so most games use both.

Singleton GameManagerEvents (C# event or ScriptableObject channel)
Question it answersWho owns the score, and how do I change it?Who needs to know the score changed?
DirectionCallers reach in: GameManager.Instance.AddScore(10)The manager reaches out: “score is now 120”
CouplingEvery caller depends on the GameManager classListeners depend only on the event (or the channel asset)
Typical misuseUI polling GameManager.Instance.GetScore() every UpdateUsing events to store state; an event says something happened, it does not remember the value

They work together. The GameManager stays the single owner of shared state and the only script that changes it. Whenever that state changes, it raises an event. UI, audio and achievements subscribe to the event instead of holding a reference to the manager or polling it every frame:

using System;
using UnityEngine;
 
public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }
 
    public event Action<int> ScoreChanged;   // listeners learn the new score
    public event Action GameOver;
 
    [SerializeField] private int score;
    public int Score => score;
 
    // Awake: the singleton guard from above
 
    public void AddScore(int amount)
    {
        if (amount <= 0) return;
        score += amount;
        ScoreChanged?.Invoke(score);         // tell, don't wait to be asked
    }
}
using UnityEngine;
using TMPro;
 
public class ScoreLabel : MonoBehaviour
{
    [SerializeField] private TMP_Text label;
 
    // Start, not OnEnable: Start only runs after every Awake, so Instance is set by then
    private void Start()
    {
        GameManager.Instance.ScoreChanged += Show;
        Show(GameManager.Instance.Score);    // draw the current value once
    }
 
    private void OnDestroy()
    {
        if (GameManager.Instance != null) GameManager.Instance.ScoreChanged -= Show;
    }
 
    private void Show(int newScore) => label.text = "Score: " + newScore;
}

The label now updates only when the score actually changes, and the GameManager does not know the label exists. The listener still names the GameManager class, though. To remove that last dependency, the manager raises a ScriptableObject event channel (for example ScoreChanged.asset) instead, and listeners reference only the asset; that also avoids the start-up ordering problem, because an asset referenced in the Inspector already exists when OnEnable runs. Unity’s PaddleBall sample goes all the way: its GameManager is not a singleton at all, but listens on some channels and broadcasts on others (Unity Technologies, Use ScriptableObjects as event channels in game code, see source-unity-scriptableobjects-modular-architecture).

Why subscribe in Start here? The Unity Scripting Reference states that Awake order between objects is not deterministic, and that Start is not called on any object until Awake has been called on every object in the scene (Unity Technologies, Scripting API: MonoBehaviour.Awake and MonoBehaviour.Start). A listener’s OnEnable can therefore run before the GameManager’s Awake has set Instance. With a ScriptableObject channel, the usual OnEnable / OnDisable pair is safe.

Rule of thumb: use the singleton (or a wired reference) to send commands to the owner of state; use events to broadcast changes from it. See unity-object-communication for the static-event version and observer-pattern for the underlying pattern.


Gotchas

  • DontDestroyOnLoad moves the GameObject into a special section of the Hierarchy visible as “DontDestroyOnLoad”. It will not appear in the scene’s own Hierarchy view.
  • Without the duplicate guard in Awake, reloading a scene that contains a GameManager will create a second instance, causing doubled score increments and other state bugs.
  • Debug.LogError is the appropriate severity for game-over and other terminal states — the red colour makes it immediately visible in the Console.
  • Mathf.Max(health - amount, 0) is equivalent to health -= amount; if (health < 0) health = 0; — prefer the Mathf version for conciseness.
  • Do not introduce the singleton before students can explain the plain Inspector-wired version. Otherwise GameManager.Instance becomes a magic phrase rather than a design decision.

Generic Singleton

The manual singleton above works for a single GameManager class, but requires re-writing the same boilerplate for every persistent manager. A generic base class eliminates that duplication (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns).

public class Singleton<T> : MonoBehaviour where T : Component
{
    private static T _instance;
 
    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindAnyObjectByType<T>();
                if (_instance == null)
                    SetupInstance();
            }
            return _instance;
        }
    }
 
    public virtual void Awake()
    {
        RemoveDuplicates();
    }
 
    private static void SetupInstance()
    {
        _instance = FindAnyObjectByType<T>();
        if (_instance == null)
        {
            GameObject gameObj = new GameObject();
            gameObj.name = typeof(T).Name;
            _instance = gameObj.AddComponent<T>();
            DontDestroyOnLoad(gameObj);
        }
    }
 
    private void RemoveDuplicates()
    {
        if (_instance == null)
            _instance = this as T;
 
        if (_instance == this)          // also true if Instance found this object before Awake ran
            DontDestroyOnLoad(gameObject);
        else
            Destroy(gameObject);        // a different instance already exists
    }
}

Any manager can then inherit from it without repeating the pattern:

public class GameManager : Singleton<GameManager>
{
    // Override Awake if needed — call base.Awake() first:
    public override void Awake()
    {
        base.Awake();
        // additional initialisation
    }
 
    // ... state fields and public methods as before
}

Trade-offs of the generic version:

  • Eliminates boilerplate for every singleton class.
  • FindAnyObjectByType (the Unity 6 replacement for the obsolete FindObjectOfType) is called when the instance is first accessed — slightly slower than a pre-assigned Instance field, but only on first access.
  • Lazy instantiation (creating the GameObject if none exists) can produce surprising objects in the Hierarchy if you access Instance before placing the manager in the scene; prefer placing the manager in the first scene rather than relying on this fallback.
  • The _instance == this check in RemoveDuplicates matters. If another script reads Instance before this object’s Awake runs, the getter has already stored this object. An unconditional else Destroy(gameObject) would delete the only manager, and a version that only calls DontDestroyOnLoad when _instance was null would skip it, so the manager would be destroyed on the next scene load. (This page’s earlier version had that bug and used the obsolete FindObjectOfType; both were corrected on 2026-10-25.)

Worked example

A minimal central controller in the Space Invaders teaching example (see overview-arcade-classics-as-learning-projects):

The plain, Inspector-wired version from the top of this page, as used in the beginner 2D route (scene set-up in the folder’s README):

  • GameManager.cs — private score, a validated AddScore and a GetScore accessor; no singleton
  • CoinPickup.cs — the coin side of the chain in the Practice below: tag check, then gameManager.AddScore(scoreValue)
  • PlayerHealth.cs — the same “private state, public method” shape applied to health on the player

Practice

Trace this interaction in words before writing code:

  1. Player overlaps a coin.
  2. CoinPickup checks the tag and calls gameManager.AddScore(10).
  3. GameManager validates the call, changes score, and logs the new value.
  4. A UI script reads the score through an accessor or receives an update method call.

Then deliberately break the Inspector reference from CoinPickup to GameManager. The expected failure is a NullReferenceException or a named Debug.LogError, depending on whether the script has a null guard. The fix starts in the Inspector, not in the scoring arithmetic.

Success test:

  • you can name which script detects the coin, which script changes the score, and which script only reads it
  • with the reference broken, you can find the empty field from the Console message alone

Extension:

  • add the ScoreChanged event from “Singleton vs events” and a ScoreLabel that subscribes to it, so the label no longer reads the score every frame

Self-test

  1. Why does a coin call gameManager.AddScore(10) instead of changing score itself?
  2. What does if (isGameOver || amount <= 0) return; protect against?
  3. In the singleton’s Awake, what does the duplicate guard prevent, and what does DontDestroyOnLoad add?
  4. When is a plain Inspector-wired reference a better choice than a singleton?
  5. A singleton and an event system answer different questions. What question does each answer?
  6. Why does ScoreLabel subscribe in Start rather than OnEnable when it uses GameManager.Instance?

Answers

  1. The GameManager owns shared state and exposes it only through public methods, so every change goes through one place where it is validated, which makes bugs easier to find.
  2. Late or nonsensical updates: changes after game over, and zero or negative amounts.
  3. Without the guard, reloading a scene that contains a GameManager creates a second instance, causing doubled score increments; DontDestroyOnLoad keeps the one instance alive across scene loads.
  4. When the game is single-scene and only a handful of scripts talk to the manager.
  5. The singleton answers “who owns the score, and how do I change it?”; events answer “who needs to know the score changed?“.
  6. Awake order between objects is not deterministic, so a listener’s OnEnable can run before the GameManager’s Awake has set Instance. Start only runs after every Awake has been called.