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