Game Manager & State Machine
Build the central brain of your game — a Game Manager that tracks what state the game is in and coordinates everything else.
What Is a Game Manager?
Every game needs a "boss" — a single script that knows what is happening in the game right now. Is the player on the main menu? Are they actively running? Did they pause? Did they die? That boss script is the Game Manager.
Think of the Game Manager like a movie director. The director does not act, operate the camera, or compose music — but they tell every department what to do and when. Your Game Manager will not move the player or spawn obstacles directly, but it will tell those systems "the game just started, do your thing" or "the player died, stop everything."
Without a Game Manager, every script has to figure out the current state on its own. The UI script checks a boolean. The obstacle spawner checks a different boolean. The player checks yet another. When these get out of sync — and they will — you get bugs like obstacles spawning on the menu screen or the player running after dying. A single source of truth prevents all of this.
Defining Game States
Before we write any code, let us think about the different "modes" our infinite runner can be in. At any given moment, the game is in exactly one of these states:
| State | What Happens |
|---|---|
| Menu | The main menu is visible. Nothing moves. The player sees a "Tap to Play" prompt. |
| Playing | The player is running. Obstacles spawn. Score increases. Input is active. |
| Paused | Everything freezes. A pause overlay appears. The player can resume or quit. |
| GameOver | The player hit an obstacle. A death animation plays. Final score is shown. The player can restart. |
We represent these states with a C# enum. An enum is simply a list of named constants — it makes your code readable and prevents typos (you cannot accidentally type "Plaiyng" like you could with strings).
namespace InfiniteRunner.Core
{
/// <summary>
/// All possible states the game can be in.
/// The game is always in exactly ONE of these states.
/// </summary>
public enum GameState
{
Menu,
Playing,
Paused,
GameOver
}
}
Create this file at Assets/Scripts/Core/GameState.cs. It is a tiny file, but keeping it separate makes it easy for any script in the project to reference these states.
The Singleton Pattern
We need the Game Manager to be accessible from anywhere — the UI needs to ask "are we paused?", the player controller needs to ask "are we playing?", the spawner needs to ask "should I spawn?". The classic solution is the Singleton pattern.
A Singleton guarantees that:
- Only one instance of the class exists in the entire game.
- That instance is accessible from anywhere via a static property (e.g.,
GameManager.Instance).
Pros: Simple, easy to access, perfect for truly global systems like a Game Manager or Audio Manager.
Cons: Can lead to tight coupling if overused. Makes unit testing harder. If you make everything a Singleton, your code becomes a tangled web of global state. Rule of thumb: use Singletons sparingly — Game Manager, Audio Manager, and maybe one or two others. For communication between systems, use events (Chapter 7).
Here is a reusable base class that any MonoBehaviour can inherit from to become a Singleton:
using UnityEngine;
namespace InfiniteRunner.Core
{
/// <summary>
/// Generic Singleton base class for MonoBehaviours.
/// Inherit from this to make any MonoBehaviour a Singleton.
/// Usage: public class GameManager : Singleton<GameManager> { }
/// </summary>
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
// The single instance of this class
private static T _instance;
// A lock object to prevent race conditions (safety measure)
private static readonly object _lock = new object();
// Has the application quit? Prevents creating instances during shutdown.
private static bool _applicationIsQuitting = false;
/// <summary>
/// Access the Singleton instance. Creates one if it does not exist.
/// </summary>
public static T Instance
{
get
{
// If the application is quitting, do not create a new instance
if (_applicationIsQuitting)
{
Debug.LogWarning(
$"[Singleton] Instance of {typeof(T)} requested after " +
"application quit. Returning null.");
return null;
}
lock (_lock)
{
if (_instance == null)
{
// Search the scene for an existing instance
_instance = FindFirstObjectByType<T>();
if (_instance == null)
{
// None found — create a new GameObject with this component
var singletonObject = new GameObject($"[{typeof(T).Name}]");
_instance = singletonObject.AddComponent<T>();
DontDestroyOnLoad(singletonObject);
}
}
return _instance;
}
}
}
protected virtual void Awake()
{
// If an instance already exists and it is not this one, destroy this duplicate
if (_instance != null && _instance != this)
{
Debug.LogWarning(
$"[Singleton] Duplicate {typeof(T).Name} detected. " +
"Destroying the new one.");
Destroy(gameObject);
return;
}
_instance = this as T;
DontDestroyOnLoad(gameObject);
}
protected virtual void OnApplicationQuit()
{
_applicationIsQuitting = true;
}
}
}
Save this file at Assets/Scripts/Core/Singleton.cs. Let us break down what every part does:
private static T _instance— The single stored reference.staticmeans it belongs to the class itself, not any particular object.where T : MonoBehaviour— A constraint that says "T must be a MonoBehaviour." This prevents someone from accidentally using this with a plain C# class.FindFirstObjectByType<T>()— Unity method that searches the entire scene for an object of this type.DontDestroyOnLoad()— Tells Unity "do not destroy this object when loading a new scene." Our Game Manager should survive scene transitions.- Duplicate check in Awake() — If we load a scene that also has a Game Manager, the duplicate destroys itself immediately.
The State Machine Pattern
A naive approach to handling game states might look like this:
// BAD: Do NOT do this! This becomes unmanageable fast.
void Update()
{
if (currentState == GameState.Menu)
{
// 50 lines of menu logic...
}
else if (currentState == GameState.Playing)
{
// 100 lines of gameplay logic...
}
else if (currentState == GameState.Paused)
{
// 30 lines of pause logic...
}
else if (currentState == GameState.GameOver)
{
// 40 lines of game over logic...
}
}
This quickly becomes a nightmare. Hundreds of lines in a single method, all tangled together. Instead, we use the State pattern: each state is its own class with its own Enter, Update, and Exit methods.
The IGameState Interface
An interface is like a contract. It says "any class that implements me must have these methods." Our interface defines what every game state must be able to do:
namespace InfiniteRunner.Core
{
/// <summary>
/// Interface that all game states must implement.
/// Each state knows how to enter, update each frame, and exit.
/// </summary>
public interface IGameState
{
/// <summary>
/// Called once when this state becomes the active state.
/// Use this for setup: show UI panels, reset variables, etc.
/// </summary>
void Enter();
/// <summary>
/// Called every frame while this state is active.
/// Use this for ongoing logic: checking input, updating timers, etc.
/// </summary>
void Update();
/// <summary>
/// Called once when leaving this state for another.
/// Use this for cleanup: hide UI panels, stop coroutines, etc.
/// </summary>
void Exit();
}
}
Save this at Assets/Scripts/Core/IGameState.cs.
Concrete State Classes
Now we create one class per state. Each class implements IGameState and holds a reference back to the Game Manager so it can trigger state transitions.
using UnityEngine;
namespace InfiniteRunner.Core.States
{
/// <summary>
/// The main menu state. Displayed when the game first opens
/// or when the player returns from a game over.
/// </summary>
public class MenuState : IGameState
{
private readonly GameManager _gameManager;
public MenuState(GameManager gameManager)
{
_gameManager = gameManager;
}
public void Enter()
{
Debug.Log("[MenuState] Entered Menu State");
// Freeze game time so nothing moves in the background
Time.timeScale = 1f;
// TODO: Show main menu UI panel
// TODO: Hide HUD (score, pause button)
// TODO: Play menu music
}
public void Update()
{
// In the menu state, we wait for the player to tap/click "Play"
// The actual button press will call GameManager.StartGame()
// so there is nothing we need to poll here yet.
}
public void Exit()
{
Debug.Log("[MenuState] Exiting Menu State");
// TODO: Hide main menu UI panel
// TODO: Stop menu music
}
}
}
using UnityEngine;
namespace InfiniteRunner.Core.States
{
/// <summary>
/// The main gameplay state. The player is running,
/// obstacles are spawning, and the score is counting up.
/// </summary>
public class PlayingState : IGameState
{
private readonly GameManager _gameManager;
public PlayingState(GameManager gameManager)
{
_gameManager = gameManager;
}
public void Enter()
{
Debug.Log("[PlayingState] Entered Playing State");
// Make sure game time is running at normal speed
Time.timeScale = 1f;
// TODO: Show gameplay HUD (score, pause button)
// TODO: Start spawning obstacles
// TODO: Enable player input
// TODO: Play gameplay music
}
public void Update()
{
// This runs every frame during gameplay.
// The GameManager's Update() calls this.
// Check for pause input (Escape key on desktop)
if (Input.GetKeyDown(KeyCode.Escape))
{
_gameManager.PauseGame();
}
}
public void Exit()
{
Debug.Log("[PlayingState] Exiting Playing State");
// TODO: Stop spawning obstacles (if transitioning to pause/gameover)
}
}
}
using UnityEngine;
namespace InfiniteRunner.Core.States
{
/// <summary>
/// The paused state. Everything freezes.
/// The player can resume or quit to the menu.
/// </summary>
public class PausedState : IGameState
{
private readonly GameManager _gameManager;
public PausedState(GameManager gameManager)
{
_gameManager = gameManager;
}
public void Enter()
{
Debug.Log("[PausedState] Entered Paused State");
// Freeze all physics and time-based movement
// timeScale = 0 stops Time.deltaTime from advancing
Time.timeScale = 0f;
// TODO: Show pause menu UI
// TODO: Mute or lower game audio
}
public void Update()
{
// Even though timeScale is 0, Update() still runs!
// We can still check for input to resume.
// Note: We use unscaledDeltaTime for anything time-based
// while paused, since deltaTime will be 0.
if (Input.GetKeyDown(KeyCode.Escape))
{
_gameManager.ResumeGame();
}
}
public void Exit()
{
Debug.Log("[PausedState] Exiting Paused State");
// Restore normal time
Time.timeScale = 1f;
// TODO: Hide pause menu UI
// TODO: Restore audio
}
}
}
using UnityEngine;
namespace InfiniteRunner.Core.States
{
/// <summary>
/// The game over state. The player has died.
/// Shows final score and options to restart or go to menu.
/// </summary>
public class GameOverState : IGameState
{
private readonly GameManager _gameManager;
public GameOverState(GameManager gameManager)
{
_gameManager = gameManager;
}
public void Enter()
{
Debug.Log("[GameOverState] Entered Game Over State");
// Slow down time for a dramatic death effect (optional)
Time.timeScale = 0.5f;
// TODO: Show game over UI with final score
// TODO: Disable player input
// TODO: Save high score
// TODO: Play death sound effect
}
public void Update()
{
// Wait for player to choose Restart or Menu.
// Buttons will call GameManager methods directly.
}
public void Exit()
{
Debug.Log("[GameOverState] Exiting Game Over State");
// Restore normal time
Time.timeScale = 1f;
// TODO: Hide game over UI
}
}
}
Each state class is small, focused, and testable. When you need to change what happens during "Game Over," you open GameOverState.cs and nothing else. Compare that to scrolling through a 500-line if/else chain in a single file. The state pattern also makes it trivial to add new states later (e.g., a Tutorial state or a Cutscene state).
The Full GameManager Script
Now we bring everything together. The Game Manager inherits from our Singleton base, creates all the state objects, and provides public methods for transitioning between them.
using System;
using UnityEngine;
using InfiniteRunner.Core.States;
namespace InfiniteRunner.Core
{
/// <summary>
/// The central manager for the entire game.
/// Tracks the current game state and provides methods to transition
/// between states. Accessible from anywhere via GameManager.Instance.
/// </summary>
public class GameManager : Singleton<GameManager>
{
// ─── Events ─────────────────────────────────────────────
// Other scripts can subscribe to this event to be notified
// whenever the game state changes.
// Example: UIManager listens and shows/hides panels accordingly.
public event Action<GameState> OnGameStateChanged;
// ─── State Machine ──────────────────────────────────────
// The state machine fields. These are created once in Awake
// and reused for the entire lifetime of the game.
private IGameState _menuState;
private IGameState _playingState;
private IGameState _pausedState;
private IGameState _gameOverState;
// The currently active state
private IGameState _currentState;
// ─── Public Properties ──────────────────────────────────
/// <summary>
/// The current game state as an enum value.
/// Useful for quick checks: if (GameManager.Instance.CurrentState == GameState.Playing)
/// </summary>
public GameState CurrentState { get; private set; } = GameState.Menu;
/// <summary>
/// How many times the player has played this session.
/// Useful for showing tutorials only on the first run.
/// </summary>
public int PlayCount { get; private set; } = 0;
// ─── Unity Lifecycle ────────────────────────────────────
protected override void Awake()
{
// IMPORTANT: Call the base Singleton.Awake() first!
// It handles the duplicate-instance check and DontDestroyOnLoad.
base.Awake();
// Create all state objects, passing a reference to this GameManager
_menuState = new MenuState(this);
_playingState = new PlayingState(this);
_pausedState = new PausedState(this);
_gameOverState = new GameOverState(this);
}
private void Start()
{
// Begin in the Menu state
TransitionToState(GameState.Menu);
}
private void Update()
{
// Delegate the Update call to whatever state is currently active.
// This is the heart of the state machine — the GameManager itself
// does not need to know what "updating" means for each state.
_currentState?.Update();
}
// ─── Public Methods (called by UI buttons, other scripts) ───
/// <summary>
/// Called when the player taps "Play" on the main menu.
/// </summary>
public void StartGame()
{
PlayCount++;
TransitionToState(GameState.Playing);
}
/// <summary>
/// Called when the player presses the pause button.
/// </summary>
public void PauseGame()
{
if (CurrentState == GameState.Playing)
{
TransitionToState(GameState.Paused);
}
}
/// <summary>
/// Called when the player presses "Resume" in the pause menu.
/// </summary>
public void ResumeGame()
{
if (CurrentState == GameState.Paused)
{
TransitionToState(GameState.Playing);
}
}
/// <summary>
/// Called when the player hits an obstacle and dies.
/// </summary>
public void EndGame()
{
if (CurrentState == GameState.Playing)
{
TransitionToState(GameState.GameOver);
}
}
/// <summary>
/// Called when the player taps "Restart" on the game over screen.
/// </summary>
public void RestartGame()
{
// Go back to Playing directly (skip the menu)
PlayCount++;
TransitionToState(GameState.Playing);
}
/// <summary>
/// Called when the player taps "Main Menu" from pause or game over.
/// </summary>
public void GoToMenu()
{
TransitionToState(GameState.Menu);
}
// ─── Private Methods ────────────────────────────────────
/// <summary>
/// Handles the transition from one state to another.
/// Calls Exit() on the old state and Enter() on the new one.
/// </summary>
private void TransitionToState(GameState newState)
{
// Exit the current state (if there is one)
_currentState?.Exit();
// Update the enum value
CurrentState = newState;
// Map the enum to the actual state object
_currentState = newState switch
{
GameState.Menu => _menuState,
GameState.Playing => _playingState,
GameState.Paused => _pausedState,
GameState.GameOver => _gameOverState,
_ => throw new ArgumentOutOfRangeException(
nameof(newState), newState,
"Unhandled game state!")
};
// Enter the new state
_currentState.Enter();
// Notify all listeners that the state has changed
OnGameStateChanged?.Invoke(newState);
Debug.Log($"[GameManager] State changed to: {newState}");
}
}
}
Save this at Assets/Scripts/Core/GameManager.cs. Let us walk through the key parts:
Line-by-Line Breakdown
public event Action<GameState> OnGameStateChanged— An event that fires whenever the state changes. Other scripts subscribe to this to react. We will build a full event system in Chapter 7, but this built-in C# event works great for the Game Manager itself.TransitionToState()— The most important method. It callsExit()on the old state, swaps the active state, then callsEnter()on the new one. This guarantees clean transitions.switch expression— ThenewState switch { ... }syntax is a modern C# feature that maps each enum value to a state object. The_at the end is a catch-all that throws an error if we somehow pass an unknown state.- Guard clauses — Methods like
PauseGame()check the current state first. You cannot pause if you are not playing. This prevents impossible state transitions.
_currentState?.Update() means "if _currentState is not null, call Update() on it." If _currentState IS null, it simply does nothing instead of crashing. This is a safety measure for the first frame before Start() runs.
Setting Up the GameManager in the Scene
Follow these steps to add the Game Manager to your Unity scene:
-
Create an empty GameObject. In the Hierarchy window, right-click and choose Create Empty. Name it
[GameManager]. The square brackets are a convention that signals "this is a manager object, not a visual thing in the scene." -
Attach the script. Select the
[GameManager]object, then in the Inspector click Add Component. Search for "GameManager" and select it. - Position does not matter. Since the Game Manager has no visual representation, its Transform position is irrelevant. Leave it at (0, 0, 0).
-
Verify DontDestroyOnLoad. Press Play. In the Hierarchy, you should see the
[GameManager]object move to a special "DontDestroyOnLoad" section. This proves it will survive scene changes.
Never place a GameManager in more than one scene. Because of DontDestroyOnLoad, the first one persists. If a second scene also has one, the Singleton base class will detect the duplicate and destroy it, but it is cleaner to only have one in your initial scene.
Testing State Transitions
Let us verify everything works. We will create a simple test script that listens for state changes and lets us trigger transitions with keyboard keys.
using UnityEngine;
using InfiniteRunner.Core;
namespace InfiniteRunner.Debug
{
/// <summary>
/// Temporary test script for verifying Game Manager state transitions.
/// Attach to any GameObject. Delete this before shipping!
/// </summary>
public class GameManagerTester : MonoBehaviour
{
private void OnEnable()
{
// Subscribe to the state change event
GameManager.Instance.OnGameStateChanged += HandleStateChanged;
}
private void OnDisable()
{
// Always unsubscribe to prevent memory leaks!
if (GameManager.Instance != null)
{
GameManager.Instance.OnGameStateChanged -= HandleStateChanged;
}
}
private void Update()
{
// Press number keys to trigger state transitions
if (Input.GetKeyDown(KeyCode.Alpha1))
{
Debug.Log("TEST: Going to Menu");
GameManager.Instance.GoToMenu();
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
Debug.Log("TEST: Starting Game");
GameManager.Instance.StartGame();
}
if (Input.GetKeyDown(KeyCode.Alpha3))
{
Debug.Log("TEST: Pausing Game");
GameManager.Instance.PauseGame();
}
if (Input.GetKeyDown(KeyCode.Alpha4))
{
Debug.Log("TEST: Resuming Game");
GameManager.Instance.ResumeGame();
}
if (Input.GetKeyDown(KeyCode.Alpha5))
{
Debug.Log("TEST: Ending Game (Player Died)");
GameManager.Instance.EndGame();
}
if (Input.GetKeyDown(KeyCode.Alpha6))
{
Debug.Log("TEST: Restarting Game");
GameManager.Instance.RestartGame();
}
}
private void HandleStateChanged(GameState newState)
{
Debug.Log($"<color=cyan>[StateChanged]</color> " +
$"Game is now in: {newState}");
}
}
}
- Create a new empty GameObject named
[Tester]. - Attach the
GameManagerTesterscript to it. - Press Play.
- Open the Console window (Window > General > Console).
- Press the number keys 1 through 6 and watch the Console output. You should see Enter and Exit messages for each state transition.
Expected Console output when you press 2 (Start Game), then 3 (Pause), then 4 (Resume), then 5 (End Game):
[MenuState] Exiting Menu State
[PlayingState] Entered Playing State
[GameManager] State changed to: Playing
[StateChanged] Game is now in: Playing
[PlayingState] Exiting Playing State
[PausedState] Entered Paused State
[GameManager] State changed to: Paused
[StateChanged] Game is now in: Paused
[PausedState] Exiting Paused State
[PlayingState] Entered Playing State
[GameManager] State changed to: Playing
[StateChanged] Game is now in: Playing
[PlayingState] Exiting Playing State
[GameOverState] Entered Game Over State
[GameManager] State changed to: GameOver
[StateChanged] Game is now in: GameOver
The GameManagerTester script is for development only. Once you have verified everything works, you can disable or delete it. In a production project, you would write proper unit tests instead (we will cover testing in a later chapter).
Project Files So Far
After this chapter, your Assets/Scripts/ folder should look like this:
Assets/
Scripts/
Core/
GameState.cs <-- The enum with Menu, Playing, Paused, GameOver
IGameState.cs <-- The interface all states implement
Singleton.cs <-- Reusable Singleton base class
GameManager.cs <-- The main Game Manager
States/
MenuState.cs <-- What happens in the menu
PlayingState.cs <-- What happens during gameplay
PausedState.cs <-- What happens when paused
GameOverState.cs <-- What happens after death
Debug/
GameManagerTester.cs <-- Temporary test script
What We Built
In this chapter, we created the backbone of our entire game:
- A GameState enum that defines all possible game states.
- A reusable Singleton base class that guarantees exactly one instance.
- An IGameState interface with Enter, Update, and Exit methods.
- Four concrete state classes that each handle their own logic cleanly.
- A GameManager that ties it all together and provides public methods for state transitions.
- An event (
OnGameStateChanged) that other systems can listen to.
In the next chapter, we will build a proper Event System using ScriptableObject event channels — a pattern used by professional Unity developers to decouple systems and keep your codebase clean.
The GameManager and state machine are working. Time to lock this in.
git status
git add .
git commit -m "Add GameManager with state machine pattern"
Three commands. Every time. It becomes second nature fast.