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") — 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;
}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.
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.
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.
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.
FindObjectOfTypeis 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.
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
Related
- unity-object-communication — the broader pattern this GameManager participates in
- 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 - source-unity-game-programming-patterns — source for the generic Singleton
implementation