Your journey through professional game patterns begins!
Today's Content
đ Based on Chapter 4
Implementing a Game Manager with the Singleton
From: Game Development Patterns with Unity 2021 (2nd Edition)
By David Baron
Remember: Baron's book is available at Dulaney Browne Library.
Today we're starting our practical pattern implementations!
Today's Agenda
What We'll Cover
Part 1: Semester Patterns Roadmap (10 patterns)
Part 2: Understanding the Singleton Pattern
Part 3: Implementing a Game Manager
Part 4: Testing the Singleton
Homework: Complete implementation & video submission
Time Management: If we run short on time, you'll complete the
implementation as homework. Either way, you'll need to test with 5 scenes!
Developer's Note: "Never Panic Early"
â ī¸ What to Expect During Implementation
As we implement this pattern, we'll be creating multiple files in a specific order.
You will see errors in Unity and your IDE until all files are complete.
How to Handle Development Errors
Don't Ignore: Note the errors - they're telling you something
Don't Panic: These are expected until all files are created
Stay Calm: Follow the implementation order and errors will resolve
As Apollo 13's Fred Haise said: "Never Panic Early"
This is a critical skill for software development. Note problems, but stay focused on the implementation path.
Part 1: Design Patterns Roadmap
10 Patterns, 10 Weeks
Each pattern solves a specific game development problem.
Each week, we'll learn the theory and implement it in Blade Racer.
The Journey
By the end of this course, you'll have a complete game built with
professional-grade architecture using industry-standard design patterns.
Don't worry about memorizing! We'll learn each pattern in context
when we need it for Blade Racer.
Part 2: The Singleton Pattern
What is a Singleton?
A class that ensures only one instance exists
and provides global access to that instance.
Real-World Analogy
Like a country having only one president at a time. Everyone knows who the
president is, and there can only be one person in that role.
The Problem Singleton Solves
â Without Singleton
// Player needs game manager
GameManager manager = GameObject.Find("GameManager")
.GetComponent<GameManager>();
// Enemy also needs game manager
GameManager manager = GameObject.Find("GameManager")
.GetComponent<GameManager>();
// UI also needs game manager... đĢ
Problems: Repeated code, slow GameObject.Find(), no guarantee of single instance
â With Singleton
// Anywhere in your code:
GameManager.Instance.StartGame();
GameManager.Instance.PauseGame();
Benefits: Clean, fast, guaranteed single instance, globally accessible
When to Use the Singleton Pattern
â Perfect For
Game Manager
Audio Manager
Input Manager
Save/Load Manager
Network Manager
Pool Manager
â Not Suitable For
Multiple instances needed
Player characters
Enemies
Projectiles
UI elements (usually)
Temporary objects
Rule of Thumb: If you need global access and only ONE instance should exist, use Singleton.
Singleton: Benefits & Drawbacks
â Benefits
Single Instance: Guaranteed only one exists
Global Access: Available from anywhere
Lazy Instantiation: Created when needed
Clean Code: No repeated GameObject.Find()
Performance: Fast direct access
â ī¸ Drawbacks
Global State: Can create hidden dependencies
Testing: Harder to unit test
Coupling: Code becomes dependent on singleton
Overuse: Can become anti-pattern if abused
Threading: Not thread-safe by default
â ī¸ Important: Don't make everything a Singleton! Use it judiciously for true manager classes.
Singleton Pattern Structure
Core Components
1. Private Static Instance
Stores the single instance of the class
Game Example: Like having a single high score tracker that stores the current record - only one "official" score needs to exist, and everyone references the same one.
2. Public Static Accessor
Provides global access point (Instance property)
Game Example: Any script can call GameManager.Instance.GetCurrentLevel() without needing to find or pass references around - just like checking what round the game is on.
3. Private Constructor (optional)
Prevents external instantiation with new keyword
When to use: Use for pure C# classes (non-MonoBehaviour) to enforce singleton pattern strictly. Skip for MonoBehaviours - Unity needs public/protected constructors for component creation. Instead, use Awake() to enforce single instance.
Game Example: A SaveManager that's pure C# would use private constructor. But GameManager (MonoBehaviour) enforces uniqueness in Awake() instead.
4. Instance Check
Creates instance if it doesn't exist (lazy instantiation)
Game Example: First time any script tries to play audio via AudioManager.Instance, the manager automatically creates itself if not already in the scene - no manual setup required.
Basic Singleton Implementation
đ Instructional Example - Manual Singleton Pattern
This code demonstrates how the Singleton pattern works internally. Don't create this file yet! This is for learning how singletons work.
Later, we'll build a better version using the generic Singleton<T> base class.
using UnityEngine;
public classGameManager : MonoBehaviour
{
// 1. Private static instance variableprivate static GameManager _instance;
// 2. Public static accessor propertypublic static GameManager Instance
{
get
{
if (_instance == null)
{
// Find existing instance in scene
_instance = FindObjectOfType<GameManager>();
if (_instance == null)
{
// Create new GameObject with GameManager
GameObject go = new GameObject("GameManager");
_instance = go.AddComponent<GameManager>();
}
}
return _instance;
}
}
voidAwake()
{
// Ensure only one instance existsif (_instance != null && _instance != this)
{
Destroy(this.gameObject);
}
else
{
_instance = this;
}
}
}
Persistent Singleton (Don't Destroy)
đ Instructional Example - Persistence Concept
This slide shows how DontDestroyOnLoad() makes objects persist across scenes.
This is still part of the learning example - we'll use this concept in our production code later.
The generic Singleton<T> class (next slide) already includes this functionality!
voidAwake()
{
if (_instance != null && _instance != this)
{
// Destroy duplicate
Destroy(this.gameObject);
}
else
{
_instance = this;
// Persist across scene loads!
DontDestroyOnLoad(this.gameObject);
}
}
DontDestroyOnLoad()
Keeps the GameObject alive when loading new scenes. Perfect for managers
that need to persist throughout the game!
Testing Note: You'll test this with 5 scenes to see it persist!
âī¸ Rider Setup (One-Time Configuration)
Configure Rider to Stop Auto-Adding Namespaces
Before we write production code, let's configure Rider so it doesn't automatically
add namespace declarations to your files. Unity scripts typically don't need namespaces
unless you're building a large-scale project.
Step-by-Step Instructions:
Open Settings: File â Settings (or Ctrl+Alt+S / Cmd+,)
Navigate to: Editor â File Templates â C#
Edit Templates: Remove namespace and corresponding curly brackets from class, interface, component, and enum.
Navigate to: Editor â File Templates â Unity
Edit Templates: Remove namespace and corresponding curly brackets from all sections that have them. It's alot. It's tedious. You only have to do it once.
Click: Save
Reopen Settings
Click the dropdown by Save
Click: This Computer
What this does: Prevents Rider from automatically wrapping your code in
namespace YourNamespace; declarations. This keeps Unity scripts clean and simple
for our course.
Generic Singleton (Reusable Template)
đ File Structure Note - PRODUCTION CODE
Create a new file: Assets/Scripts/Core/Singleton.cs
This is a reusable generic base class that ANY class can inherit from to become a singleton.
This is the recommended approach in production! â ī¸ This code goes into your Unity project and will be used throughout Blade Racer.
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : Component
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
if (_instance == null)
{
GameObject obj = new GameObject();
obj.name = typeof(T).Name;
_instance = obj.AddComponent<T>();
}
}
return _instance;
}
}
public virtual voidAwake()
{
if (_instance == null)
{
_instance = this as T;
DontDestroyOnLoad(gameObject);
}
else { Destroy(gameObject); }
}
}
Understanding the Class Declaration
public class Singleton<T> : MonoBehaviour where T : Component
Singleton<T> - Generic class with type parameter T
Means: T is a placeholder for any type you specify later
: MonoBehaviour - Inherits from Unity's MonoBehaviour
Means: Can be attached to GameObjects like any Unity script
where T : Component - Generic constraint
Means: T must be a Component type (MonoBehaviour, Transform, etc.)
public virtual void Awake() - Virtual method
Means: Child classes can override this method to add their own initialization logic
Why? GameManager needs its own Awake() code, but must also call base.Awake() to ensure singleton setup runs first
Example Usage:
class GameManager : Singleton<GameManager>
Here T = GameManager
Generics Power! Remember from our crash course? Now any class can become a Singleton!
Using the Generic Singleton
đ Instructional Example
GameManager - Production Code you will build in a few slides. Not now. AudioManager & Player - Example code only (demonstrates the pattern, not for Blade Racer)
These examples show how ANY manager class can use the Singleton pattern.
// Simply inherit from Singleton<T>public classGameManager : Singleton<GameManager>
{
public voidStartGame()
{
Debug.Log("Game Started!");
}
}
public classAudioManager : Singleton<AudioManager>
{
public voidPlaySound(string soundName)
{
Debug.Log("Playing: " + soundName);
}
}
// Use them anywhere!public classPlayer : MonoBehaviour
{
voidStart()
{
GameManager.Instance.StartGame();
AudioManager.Instance.PlaySound("PlayerSpawn");
}
}
Part 3: Designing a Game Manager
What Does a Game Manager Do?
đŽ Initialize game systems
â¸ī¸ Handle game state (Start, Pause, Resume, End)
đ Track game-wide data (score, time, level)
đ Coordinate between systems
đž Manage scene transitions
For Blade Racer: Our Game Manager will initialize the race,
track time, handle pause/resume, and manage race completion.
Game Manager Implementation
đ File Structure Note - PRODUCTION CODE
All code in the next several slides goes into: Assets/Scripts/Managers/GameManager.cs â ī¸ Build this complete GameManager for your Blade Racer project.
Building It Step by Step
Let's break down the Game Manager implementation piece by piece.
Each section serves a specific purpose in managing your game's lifecycle.
What We'll Build
Class Setup: State variables and inheritance
Initialization: Awake and Start lifecycle
Game Control: Start and Pause methods
Game Loop: Resume and Update tracking
Follow Along: Next slides show each piece with explanations!
Game Manager - Class Setup
using UnityEngine;
public classGameManager :
Singleton<GameManager>
{
// Game state variablesprivate bool _isGameActive;
private float _gameTime;
// Methods go here...
}
How This Works
Inheritance: By inheriting from Singleton<GameManager>,
we get all the singleton behavior automatically!
State Variables:
_isGameActive - Tracks if game is running or paused
_gameTime - Tracks total elapsed game time
Game Use: In Blade Racer, these track whether the race is active
and how long the player has been racing.
Awake(): We override Awake but call base.Awake()
to ensure the singleton setup happens first.
Start(): Unity calls this after all Awake methods complete.
Perfect for initialization!
InitializeGame(): Sets default values - game starts paused
with time at zero.
Game Use: When Blade Racer loads, this prepares the game state
before the race countdown begins.
Game Manager - Game Control
public voidStartGame()
{
Debug.Log("[GameManager] Game Started!");
_isGameActive = true;
}
public voidPauseGame()
{
Debug.Log("[GameManager] Game Paused");
_isGameActive = false;
Time.timeScale = 0f;
}
How This Works
StartGame(): Sets _isGameActive to true,
allowing the game loop to run. Called when race begins!
PauseGame(): Stops game and sets Time.timeScale = 0
which freezes all time-based operations in Unity.
Game Use: Any script can call GameManager.Instance.PauseGame()
when ESC is pressed. UI, enemies, physics - everything pauses!
Game Manager - Game Loop
public voidResumeGame()
{
Debug.Log("[GameManager] Game Resumed");
_isGameActive = true;
Time.timeScale = 1f;
}
voidUpdate()
{
if (_isGameActive)
{
_gameTime += Time.deltaTime;
}
}
How This Works
ResumeGame(): Opposite of pause - reactivates the game
and restores normal time flow with Time.timeScale = 1.
Update(): Runs every frame. Only tracks time when game is active,
using Time.deltaTime (time since last frame).
Game Use: Track total race time in Blade Racer. Display on UI,
use for leaderboards, or trigger time-based events.
C# Concept: The 'override' Keyword
đ Instructional Example - C# Language Concept
This slide explains the C# override keyword used in your production GameManager code.
The examples here are for learning purposes - you already used this in your GameManager.cs.
// In Singleton<T> base class:public virtual voidAwake()
{
// Singleton setup code...
}
Override lets a child class replace or extend a method
from its parent class.
The Rules:
Parent class marks method as virtual (meaning "you can override this")
Child class uses override to replace that method
Use base.MethodName() to call the parent's version first
Why we need it: Singleton's Awake() sets up the singleton behavior.
GameManager's Awake() needs to do that AND add custom setup.
Override lets us do both!
Override in Action: The Flow
What Happens When GameManager Starts?
Step 1: Unity calls Awake()
Unity sees GameManager component and calls its Awake() method
Step 2: GameManager.Awake() runs
First line is base.Awake() - this calls the parent (Singleton) version
public override voidAwake()
{
base.Awake(); â Jumps to parent!
}
Step 3: Singleton<T>.Awake() runs
Checks if instance exists, sets DontDestroyOnLoad, handles duplicates
Step 4: Returns to GameManager.Awake()
After parent finishes, continues with any custom code below base.Awake()
Key Insight: Always call base.Awake() FIRST so the singleton setup
happens before your custom code runs!
Result: C# compiler error! Can't redefine a method
without 'override' keyword.
Error message: "...hides inherited member. Use the new keyword
if hiding was intended, or use override."
â With Override
// Correct!public override voidAwake()
{
base.Awake();
// Parent runs first â// Then our code â
}
Result: Works perfectly! Singleton setup happens,
then your custom code runs.
Best Practice: Visual Studio Code will suggest 'override'
when you start typing parent methods.
Remember: If parent has virtual, child needs override!
Unity Concept: Time.deltaTime & Time.timeScale
đ Instructional Example - Unity API Explanation
This slide explains Unity's Time system used in your production GameManager code.
The examples here demonstrate concepts - you already use Time.deltaTime and Time.timeScale in GameManager.cs.
Time.deltaTime
What it is: The time (in seconds) since the last frame was rendered.
Typical value: Around 0.016 seconds (60 FPS) or 0.033 seconds (30 FPS)
voidUpdate()
{
// Track total time
_gameTime += Time.deltaTime;
// Move at 5 units per second
transform.position +=
Vector3.forward * 5f * Time.deltaTime;
}
Why use deltaTime? Makes movement frame-rate independent!
Fast PC (120 FPS) and slow PC (30 FPS) move at same speed.
Time.timeScale
What it is: A multiplier for how fast time passes in Unity.
Default value: 1.0 (normal speed)
// Normal speed
Time.timeScale = 1f;
// Slow motion (50% speed)
Time.timeScale = 0.5f;
// Fast forward (2x speed)
Time.timeScale = 2f;
// Freeze time (PAUSE!)
Time.timeScale = 0f;
âĸ Time.deltaTime equals real frame time (~0.016s at 60 FPS)
âĸ Everything runs as expected
Time.timeScale = 0.5f (SLOW MOTION)
âĸ Game runs at half speed
âĸ Time.deltaTime is halved (0.008s instead of 0.016s)
âĸ Great for bullet-time effects!
Time.timeScale = 2f (FAST FORWARD)
âĸ Game runs at double speed
âĸ Time.deltaTime is doubled (0.032s instead of 0.016s)
âĸ Useful for speed-up power-ups or simulation fast-forward
Putting It Together: Pause & Resume
// Our PauseGame() method:public voidPauseGame()
{
Debug.Log("[GameManager] Game Paused");
_isGameActive = false;
Time.timeScale = 0f; â Freeze time!
}
// Our ResumeGame() method:public voidResumeGame()
{
Debug.Log("[GameManager] Game Resumed");
_isGameActive = true;
Time.timeScale = 1f; â Restore time!
}
// Update() respects the pause:voidUpdate()
{
if (_isGameActive) â Optional check
{
// Even if this runs, deltaTime = 0
_gameTime += Time.deltaTime;
}
}
How The Pause Works
Step 1: Player presses ESC, calls PauseGame()
Step 2: Set _isGameActive = false (our custom flag)
Step 3: Set Time.timeScale = 0 (Unity's time freeze)
Result: All physics, animations, and time-based code freezes!
Game Example: In Blade Racer, when you pause:
Bike stops moving (physics frozen)
Wheel animations freeze
Enemies stop chasing
Timer stops counting
BUT: UI still works (canvas updates independently!)
Pro Tip: You can use Time.unscaledDeltaTime for things that should
keep running during pause (like UI animations or pause menu effects)!
Part 4: Testing the Singleton
Testing Strategy
Create Test Script: TestGameManager.cs
Call Instance: Verify global access works
Test Methods: Call StartGame(), PauseGame(), etc.
Multiple Scenes: Load 5 different scenes
Verify Persistence: Confirm same instance across scenes
Your Homework: Create 5 test scenes and verify the Game Manager
persists across all of them!
Test Script Example
đ File Structure Note - TESTING CODE
Create a new file: Assets/Scripts/Testing/TestGameManager.cs
This script is for testing your Singleton implementation across multiple scenes. â ī¸ This is temporary testing code - create it for your homework assignment, then you can remove it later.
using UnityEngine;
using UnityEngine.SceneManagement;
public classTestGameManager : MonoBehaviour
{
voidStart()
{
// Test global access
Debug.Log("Testing GameManager Singleton...");
// Start the game
GameManager.Instance.StartGame();
}
voidUpdate()
{
// Test pause/resumeif (Input.GetKeyDown(KeyCode.P))
{
GameManager.Instance.PauseGame();
}
if (Input.GetKeyDown(KeyCode.R))
{
GameManager.Instance.ResumeGame();
}
// Load next scene to test persistenceif (Input.GetKeyDown(KeyCode.N))
{
int nextScene = SceneManager.GetActiveScene().buildIndex + 1;
if (nextScene < SceneManager.sceneCountInBuildSettings)
{
SceneManager.LoadScene(nextScene);
}
}
}
}
Setting Up Test Scenes
Step-by-Step Setup
Create 5 new scenes: Scene1, Scene2, Scene3, Scene4, Scene5
Add all scenes to Build Profiles (File â Build Profiles)
In Scene1: Add GameObject with GameManager script
In each scene: Add different colored background (visual verification)
In each scene: Add GameObject with TestGameManager script
Add UI Text showing scene name (optional but helpful)
Visual Test: Different background colors help you see when scenes change!
Testing Checklist
â Verify These Behaviors
â GameManager.Instance is accessible from any script
â Only ONE instance exists (check Hierarchy)
â Instance persists across scene loads
â StartGame() logs correctly
â PauseGame() freezes time (press P)
â ResumeGame() restores time (press R)
â Scene loads work (press N for next scene)
Common Singleton Issues
â Common Problems
Multiple instances: Check Awake() logic
Null reference: Instance accessed before creation
Destroyed on load: Missing DontDestroyOnLoad()
Wrong instance: Not calling Destroy() on duplicates
â Solutions
Destroy duplicates: if (_instance != this)
Lazy instantiation: Create in getter if null
Persist properly: DontDestroyOnLoad(gameObject)
Debug logs: Verify Awake() calls
Debug Tip: Add Debug.Log() in Awake() to see when instances are created/destroyed!
Note: If we don't finish in class, complete this as homework!
Gaming History Moment đšī¸
Nintendo NES & Quality Control (1985)
After the 1983 video game crash nearly destroyed the industry, retailers refused to stock game consoles. Nintendo faced an uphill battle launching the NES in North America in 1985. Their solution? The Nintendo Seal of Quality - a strict quality control program where Nintendo tested and approved every game before release.
Unlike Atari's open platform (which led to disasters like E.T.), Nintendo implemented a lockout chip that prevented unauthorized cartridges from running. Publishers had to go through Nintendo for approval, limiting releases to maintain quality. Only ONE version of each game could exist on the platform.
Connection to Singleton Pattern
Nintendo's Seal of Quality is like the Singleton pattern - ensuring only ONE official, approved instance exists. Just as our GameManager ensures only one instance manages the game state, Nintendo ensured only one approved version of each game reached consumers. This "single source of truth" approach saved the video game industry!