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:
- The fields hold shared game state: score, health, lives and game-over status.
- Other scripts do not edit those fields. A coin calls
AddScore; a hazard callsTakeDamage; a health pack callsHeal. - Each public method checks whether the call is valid before changing state.
if (isGameOver || amount <= 0) return;prevents late or nonsensical updates. - Private helper logic such as
LoseLifeis hidden inside the GameManager because other scripts do not need to know how losing a life works. - Accessors such as
GetScorelet 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 GameManager | Events (C# event or ScriptableObject channel) | |
|---|---|---|
| Question it answers | Who owns the score, and how do I change it? | Who needs to know the score changed? |
| Direction | Callers reach in: GameManager.Instance.AddScore(10) | The manager reaches out: “score is now 120” |
| Coupling | Every caller depends on the GameManager class | Listeners depend only on the event (or the channel asset) |
| Typical misuse | UI polling GameManager.Instance.GetScore() every Update | Using 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
DontDestroyOnLoadmoves 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.LogErroris 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 tohealth -= amount; if (health < 0) health = 0;— prefer theMathfversion for conciseness.- Do not introduce the singleton before students can explain the plain Inspector-wired version. Otherwise
GameManager.Instancebecomes 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 obsoleteFindObjectOfType) is called when the instance is first accessed — slightly slower than a pre-assignedInstancefield, but only on first access.- Lazy instantiation (creating the GameObject if none exists) can produce surprising objects in the Hierarchy if you access
Instancebefore placing the manager in the scene; prefer placing the manager in the first scene rather than relying on this fallback. - The
_instance == thischeck inRemoveDuplicatesmatters. If another script readsInstancebefore this object’sAwakeruns, the getter has already stored this object. An unconditionalelse Destroy(gameObject)would delete the only manager, and a version that only callsDontDestroyOnLoadwhen_instancewas 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 obsoleteFindObjectOfType; 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):
- SpaceInvadersGame.cs — owns score, lives, and a
Stateenum, routing every game event through it
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 validatedAddScoreand aGetScoreaccessor; 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:
- Player overlaps a coin.
CoinPickupchecks the tag and callsgameManager.AddScore(10).GameManagervalidates the call, changesscore, and logs the new value.- 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
ScoreChangedevent from “Singleton vs events” and aScoreLabelthat subscribes to it, so the label no longer reads the score every frame
Self-test
- Why does a coin call
gameManager.AddScore(10)instead of changingscoreitself? - What does
if (isGameOver || amount <= 0) return;protect against? - In the singleton’s
Awake, what does the duplicate guard prevent, and what doesDontDestroyOnLoadadd? - When is a plain Inspector-wired reference a better choice than a singleton?
- A singleton and an event system answer different questions. What question does each answer?
- Why does
ScoreLabelsubscribe inStartrather thanOnEnablewhen it usesGameManager.Instance?
Answers
- 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.
- Late or nonsensical updates: changes after game over, and zero or negative amounts.
- Without the guard, reloading a scene that contains a GameManager creates a second instance, causing doubled score increments;
DontDestroyOnLoadkeeps the one instance alive across scene loads. - When the game is single-scene and only a handful of scripts talk to the manager.
- The singleton answers “who owns the score, and how do I change it?”; events answer “who needs to know the score changed?“.
Awakeorder between objects is not deterministic, so a listener’sOnEnablecan run before the GameManager’sAwakehas setInstance.Startonly runs after everyAwakehas been called.
Related
- unity-object-communication — the broader pattern this GameManager participates in
- unity-scene-management — loading, restarting and win/lose scene flow,
sceneLoaded, and theTime.timeScalereset - unity-inspector-references — wiring GameManager references to other scripts
- unity-prefabs-scripting — spawner scripts that call
gameManager.AddScore - unity-collider2d-and-triggers — pickup scripts that call
TakeDamageandAddScore - scriptableobject-event-channels — event channels that let listeners react without any reference to the manager
- observer-pattern — the pattern behind the manager’s events
- source-unity-game-programming-patterns — source for the generic Singleton
implementation - singleton-pattern — Singleton pattern in general: cautions and alternatives