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.

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") — covered in Week 7
        }
        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;
}

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.


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.

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 = (T)FindObjectOfType(typeof(T));
                if (_instance == null)
                    SetupInstance();
            }
            return _instance;
        }
    }
 
    public virtual void Awake()
    {
        RemoveDuplicates();
    }
 
    private static void SetupInstance()
    {
        _instance = (T)FindObjectOfType(typeof(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;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

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.
  • 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.