📝 Lesson 4.1: Game State and Managers
Somewhere, something has to know the score, whether the game is paused, and which level loads next. That's a manager — a central coordinator. This lesson covers the manager and singleton patterns, a clean game state machine, keeping managers alive across scenes, and loading scenes from code.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain the role of a manager and implement the Unity singleton pattern
- Persist a manager across scene loads with
DontDestroyOnLoad - Model game flow as a state machine (MainMenu → Playing → Paused → GameOver)
- Pause the game with
Time.timeScaleand react to state changes - Load and switch scenes with
SceneManager(single & additive) - Weigh singletons against events (Lesson 3.2) and avoid their pitfalls
Estimated Time: 60 minutes
Project: A GameManager singleton that tracks state and score, persists across scenes, and drives pause and restart.
In This Lesson
What Is a Manager?
As a game grows, some responsibilities don't belong to any single GameObject. "The current score" isn't the player's job or the UI's job — it's game-wide state. A manager is a component that owns one such cross-cutting concern and offers a central place to read and change it.
📖 Definition
A manager is a coordinating MonoBehaviour responsible for a global concern — GameManager (overall state, score), AudioManager (music/SFX), SceneLoader (transitions), SaveManager (persistence). Typically there's exactly one of each in the game, which is why managers so often become singletons.
The challenge: if there's one GameManager, how does the player, the UI, an enemy — anyone — reach it? You could wire a [SerializeField] reference into every script (Lesson 1.3), but that's tedious for something everything needs. The singleton pattern gives one globally-reachable access point.
The Singleton Pattern
A singleton guarantees a class has exactly one instance and exposes it through a static Instance property, so any script can call GameManager.Instance.AddScore(10) from anywhere.
using UnityEngine;
public class GameManager : MonoBehaviour
{
// The single, globally-accessible instance.
public static GameManager Instance { get; private set; }
public int Score { get; private set; }
void Awake()
{
// Enforce a single instance: if one already exists, destroy this duplicate.
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
public void AddScore(int amount)
{
Score += amount;
Debug.Log($"Score: {Score}");
}
}
(the one manager)"] P["Player"] -->|"AddScore(10)"| GM E["Enemy"] -->|"AddScore(50)"| GM U["ScoreUI"] -->|"reads Score"| GM style GM fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style P fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style E fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
💡 Why register in Awake?
From Lesson 1.2: all Awake calls run before any Start. Registering Instance = this in Awake guarantees the singleton is ready before any other script's Start tries to use it. That's the two-phase startup rule paying off — set yourself up in Awake, use others in Start.
⚠️ The duplicate guard matters
The if (Instance != null) check destroys accidental duplicates — crucial once you add DontDestroyOnLoad (next), because reloading a scene that contains the manager would otherwise create a second one. Expose Instance with a private set so outside code can read it but never reassign it.
Surviving Scene Loads
By default, loading a new scene destroys every GameObject in the old one — including your manager. But score, settings, and audio should carry from the menu into the level and between levels. DontDestroyOnLoad marks an object to survive scene changes.
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject); // this manager persists across all scenes
}
✅ The persistent-singleton recipe
Put the manager in your first scene, register Instance in Awake, guard against duplicates, and call DontDestroyOnLoad(gameObject). Now it's a single object that lives for the whole session and is reachable everywhere via Instance. The duplicate guard is what stops a second copy appearing if you return to the first scene.
⚠️ Important: DontDestroyOnLoad only works on root GameObjects. If your manager is a child of another object, it won't persist — Unity even warns you. Keep managers at the scene root.
A Game State Machine
Games move through distinct states — a main menu, active play, a pause screen, game over — and each allows different things. Scattering bool isPaused, bool isGameOver flags everywhere quickly becomes a tangle of impossible combinations. A state machine models this cleanly with a single enum.
public enum GameState { MainMenu, Playing, Paused, GameOver }
The manager holds the current state, exposes a method to change it, and raises an event (Lesson 3.2) so UI and systems react — without the manager knowing about them:
using System;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
public GameState State { get; private set; } = GameState.MainMenu;
public event Action<GameState> StateChanged; // announce transitions
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
DontDestroyOnLoad(gameObject);
}
public void SetState(GameState newState)
{
if (State == newState) return;
State = newState;
// Pause freezes time-based logic; WaitForSeconds & physics stop (Lesson 3.3 / 1.2).
Time.timeScale = (newState == GameState.Paused) ? 0f : 1f;
StateChanged?.Invoke(newState); // let listeners react
}
}
💡 Time.timeScale is the pause switch
Setting Time.timeScale = 0 freezes Time.deltaTime, physics, and WaitForSeconds — most gameplay halts without any per-object code. Set it back to 1 to resume. Remember (Lesson 3.3): menu animations that must run while paused should use unscaled time or WaitForSecondsRealtime.
Scene Management
Loading levels, returning to the menu, restarting after game over — all handled by the SceneManager class in the UnityEngine.SceneManagement namespace.
using UnityEngine;
using UnityEngine.SceneManagement; // required for SceneManager
public class SceneFlow : MonoBehaviour
{
public void LoadLevel(string sceneName) => SceneManager.LoadScene(sceneName);
public void Restart()
{
// Reload the scene that's currently active.
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
public void QuitToMenu() => SceneManager.LoadScene("MainMenu");
}
| Call | Effect |
|---|---|
LoadScene("Level1") | Replaces the current scene(s) with the named one. |
LoadScene(index) | Same, by build index (from Build Settings). |
LoadScene(name, LoadSceneMode.Additive) | Loads a scene alongside the current one (streaming, overlays). |
LoadSceneAsync(...) | Loads in the background so you can show a progress bar (pairs with coroutines/async, Lesson 3.3). |
⚠️ Scenes must be in Build Settings
LoadScene can only load scenes added to File → Build Settings → Scenes In Build. A scene that exists in your Project but isn't in that list fails at runtime. Also: resetting Time.timeScale = 1 when leaving a paused state is essential — load a scene while timeScale is still 0 and the new scene appears frozen.
💡 Additive scenes and managers: a common architecture keeps managers in a small persistent "bootstrap" scene loaded additively, with levels swapped in and out around it. Combined with DontDestroyOnLoad, this cleanly separates "systems that live forever" from "content that comes and goes."
Singleton Pitfalls
Singletons are convenient — maybe too convenient. Because Instance is reachable from anywhere, it's tempting to route everything through it, recreating the tight coupling Lesson 3.2 warned against. Used well, singletons and events complement each other.
| Pitfall | Why it hurts | Mitigation |
|---|---|---|
| Global access everywhere | Everything depends on the singleton — hard to reuse or reason about | Let the manager raise events; listeners react (don't have listeners poll the singleton) |
| Hidden dependencies | GameManager.Instance buried in a method isn't visible in the Inspector | Prefer a [SerializeField] reference when a specific object is known |
| Hard to test | Static state persists between tests | Keep managers thin; put logic in plain, testable classes (Lesson 5.2) |
| Too many managers | A "GodManager" that does everything | One manager per concern; split responsibilities |
✅ A healthy balance
Use a singleton for genuinely global, single-instance services (the game manager, audio, scene flow). Have it broadcast state changes via events so most systems react without reaching back in. Reserve direct Instance calls for deliberate commands ("add score", "load level"). That mix keeps a central authority and loose coupling.
💡 Rule of thumb: if you find everything calling Instance, you've drifted back to tight coupling. Push notifications out with events; pull commands in sparingly.
Exercise & Quiz
🏋️ Exercise: A Persistent GameManager
Objective: Build a singleton GameManager that tracks state and score, persists across scenes, and drives pause and restart.
Instructions:
- Create the
GameStateenum and aGameManagersingleton (Sections 2–4) with theAwakeguard andDontDestroyOnLoad. - Add a
Scoreproperty andAddScore(int); raise an event when it changes. - Add
SetState(GameState)that setsTime.timeScalefor pause and raisesStateChanged. - On
Escape, toggle betweenPlayingandPaused. On a "Restart" call, reset score and reload the active scene. - Put the manager in your first scene, add a second scene to Build Settings, and confirm the score survives a scene load.
Starter Code:
using System;
using UnityEngine;
using UnityEngine.SceneManagement;
public enum GameState { MainMenu, Playing, Paused, GameOver }
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
public GameState State { get; private set; } = GameState.MainMenu;
public int Score { get; private set; }
public event Action<GameState> StateChanged;
public event Action<int> ScoreChanged;
void Awake()
{
// TODO: singleton guard + DontDestroyOnLoad.
}
// TODO: AddScore, SetState (with timeScale), Restart.
}
💡 Hint
Guard: if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject);. In SetState, set Time.timeScale = newState == GameState.Paused ? 0f : 1f; then StateChanged?.Invoke(newState). In Restart, reset Score, set state to Playing (which restores timeScale), then SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex).
✅ Solution
using System;
using UnityEngine;
using UnityEngine.SceneManagement;
public enum GameState { MainMenu, Playing, Paused, GameOver }
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
public GameState State { get; private set; } = GameState.MainMenu;
public int Score { get; private set; }
public event Action<GameState> StateChanged;
public event Action<int> ScoreChanged;
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
DontDestroyOnLoad(gameObject);
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
if (State == GameState.Playing) SetState(GameState.Paused);
else if (State == GameState.Paused) SetState(GameState.Playing);
}
}
public void AddScore(int amount)
{
Score += amount;
ScoreChanged?.Invoke(Score);
}
public void SetState(GameState newState)
{
if (State == newState) return;
State = newState;
Time.timeScale = (newState == GameState.Paused) ? 0f : 1f;
StateChanged?.Invoke(newState);
}
public void Restart()
{
Score = 0;
SetState(GameState.Playing); // also restores timeScale to 1
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}
One central authority, reachable via Instance, that announces state and score changes through events. UI and systems subscribe (Lesson 3.2) — they never poll the manager. It persists across scenes, and Restart cleanly resets both score and time.
🎯 Quick Quiz
Question 1: Why does the singleton register Instance = this in Awake rather than Start?
Question 2: What does DontDestroyOnLoad(gameObject) do?
Question 3: What's the recommended way to keep managers from recreating the tight coupling of Lesson 3.2?
Summary
🎉 Key Takeaways
- A manager owns a global concern (state, score, audio, scenes); typically one per game, so managers are often singletons.
- The singleton pattern: a static
Instance(withprivate set), registered inAwake, with a duplicate guard that destroys extra copies. DontDestroyOnLoad(gameObject)(on a root object) keeps a manager alive across scene loads — the persistent-singleton recipe.- Model game flow as a state machine with an
enum;Time.timeScale = 0pauses time-based logic. Raise an event on transitions. SceneManager.LoadScene(single/additive/async) switches scenes — but scenes must be in Build Settings, and resettimeScaleto1before loading.- Avoid singleton overuse: broadcast via events, command via
Instancesparingly, one manager per concern.
📚 Additional Resources
- Scripting Reference — DontDestroyOnLoad
- Scripting Reference — SceneManager
- Scripting Reference — Time.timeScale
- Unity — Programming patterns for game architecture
🚀 What's Next?
Your game now has a brain that tracks state and score. Next, players need to see it. Lesson 4.2, UI Scripting, connects your managers and events to on-screen UI — score labels, health bars, pause menus — with uGUI and UI Toolkit.
🎉 Your game has a control center!
State, score, scenes, pause — all coordinated from one clear place. That's the backbone every shippable game needs.