C# Singleton Pattern: A Practical Guide for Unity Developers

The Singleton pattern is one of the most commonly used design patterns in Unity projects. It provides a way to ensure that a class has only one shared instance and gives other systems a convenient way to access it.

For small projects, a Singleton can be extremely useful. However, using Singleton everywhere can make a project difficult to maintain.

In this guide, we’ll look at what the Singleton pattern is, how to implement it in C#, how it works in Unity, and when you should—or shouldn’t—use it.


What Is the Singleton Pattern?

A Singleton ensures that a class has only one instance during its lifetime.

Conceptually:

Game
 │
 ├── GameManager
 │      ↓
 │   ONE INSTANCE
 │
 ├── Player
 │
 └── Enemy

Instead of creating multiple GameManager objects, other systems access the same instance.

For example:

GameManager.Instance.StartGame();

The important parts are:

  1. The constructor or creation process is controlled.
  2. Only one instance is allowed.
  3. The instance is globally accessible.

A Basic C# Singleton

A simple implementation looks like this:

public class GameManager
{
    private static GameManager instance;

    public static GameManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new GameManager();
            }

            return instance;
        }
    }

    private GameManager()
    {
    }
}

Now you can access the class through:

GameManager.Instance

For example:

GameManager.Instance.StartGame();

The constructor is private, preventing other classes from directly creating another GameManager.


Why Use a Singleton?

Singletons are useful when a system genuinely represents a single global service.

Common examples include:

  • Game Manager
  • Audio Manager
  • Save Manager
  • Settings Manager
  • Input Manager
  • Achievement Manager
  • Analytics Manager
  • Configuration Manager

For example, your game may only need one audio system.

AudioManager
      │
 ┌────┼────┐
 ↓    ↓    ↓
Menu Player Enemy

All systems can communicate with the same Audio Manager.


Singleton in Unity

Unity introduces an additional consideration because MonoBehaviour objects are managed by the Unity engine.

A common Unity Singleton looks like this:

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
    }
}

Now other scripts can access:

GameManager.Instance

Understanding Awake()

The Awake() method runs when Unity initializes the object.

This is where we check whether another instance already exists.

if (Instance != null && Instance != this)
{
    Destroy(gameObject);
    return;
}

If another instance exists, the new object is destroyed.

Otherwise:

Instance = this;

registers the current object as the Singleton.


Using DontDestroyOnLoad()

Normally, Unity destroys scene objects when you load another scene.

For example:

Main Menu
    ↓
Gameplay

Objects in the Main Menu scene are normally destroyed.

If your GameManager needs to survive scene changes:

DontDestroyOnLoad(gameObject);

tells Unity to keep it alive.

The structure becomes:

Scene 1
  │
  └── GameManager
          │
          ↓
      Scene Change
          │
          ↓
Scene 2
  │
  └── Same GameManager

This is useful for persistent systems such as audio, save data, and game state.


The Duplicate Instance Problem

A common problem occurs when you put a Singleton prefab into multiple scenes.

For example:

Scene 1
 └── GameManager

Scene 2
 └── GameManager

When Scene 2 loads, Unity may create another GameManager.

The Singleton protects against this:

if (Instance != null && Instance != this)
{
    Destroy(gameObject);
    return;
}

The first instance survives.

The duplicate is destroyed.


A Better Generic Singleton

If your project contains several Singleton systems, you don’t necessarily want to repeat the same code.

You can create a generic base class:

using UnityEngine;

public abstract class Singleton<T> : MonoBehaviour
    where T : MonoBehaviour
{
    public static T Instance { get; private set; }

    protected virtual void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this as T;

        DontDestroyOnLoad(gameObject);
    }
}

Then:

public class AudioManager : Singleton<AudioManager>
{
}

And:

public class SaveManager : Singleton<SaveManager>
{
}

You can access them with:

AudioManager.Instance
SaveManager.Instance

This reduces duplicated Singleton code.


Example: Audio Manager

A simple Audio Manager might look like:

using UnityEngine;

public class AudioManager : MonoBehaviour
{
    public static AudioManager Instance { get; private set; }

    [SerializeField]
    private AudioSource musicSource;

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public void PlayMusic(AudioClip clip)
    {
        musicSource.clip = clip;
        musicSource.Play();
    }
}

Another script can then call:

AudioManager.Instance.PlayMusic(battleMusic);

This is convenient because the caller doesn’t need to know where the AudioManager exists.


The Biggest Problem With Singletons

Singletons are convenient—but convenience can become a problem.

Consider:

GameManager.Instance
AudioManager.Instance
SaveManager.Instance
UIManager.Instance
PlayerManager.Instance
InventoryManager.Instance
EnemyManager.Instance
QuestManager.Instance
NetworkManager.Instance

Eventually, almost every system becomes globally accessible.

This creates global state.

Your classes become tightly coupled.

For example:

public class Enemy
{
    public void Die()
    {
        GameManager.Instance
            .UIManager
            .ScoreManager
            .AudioManager
            .SaveManager
            .UpdateSomething();
    }
}

Now Enemy depends on many global systems.

That makes the code harder to:

  • Test
  • Refactor
  • Reuse
  • Debug
  • Understand

Singleton vs Dependency Injection

A cleaner architecture for larger projects can use Dependency Injection (DI).

Instead of:

AudioManager.Instance.PlaySound(sound);

you could provide the dependency:

public class Player
{
    private readonly IAudioService audioService;

    public Player(IAudioService audioService)
    {
        this.audioService = audioService;
    }
}

Now the Player doesn’t need to know how the audio system is created.

This reduces coupling.

The general idea is:

Singleton

Player → Global AudioManager

versus:

Dependency Injection

AudioService → Player

The second approach becomes increasingly valuable as the project grows.


When Should You Use a Singleton?

Singletons are a reasonable choice when:

✔ There should genuinely be one instance

For example, one application-level audio service.

✔ The system has global scope

For example, global game configuration.

✔ The system has a long lifetime

For example, a persistent save service.

✔ The convenience outweighs the coupling

For small and medium-sized projects, this can be a practical trade-off.


When Should You Avoid a Singleton?

Avoid creating a Singleton simply because accessing an object is convenient.

Don’t turn every manager into:

SomethingManager.Instance

You should especially be cautious when:

  • Objects have different lifetimes
  • Multiple instances may be useful later
  • You need strong testability
  • Systems are tightly coupled
  • Dependencies are unclear
  • You’re building a large architecture

A Singleton isn’t automatically good architecture.


Singleton vs Static Class

These two concepts are often confused.

A static class:

public static class GameUtility
{
    public static int ClampHealth(int value)
    {
        return Mathf.Clamp(value, 0, 100);
    }
}

doesn’t have an instance.

You use:

GameUtility.ClampHealth(150);

A Singleton has an actual object instance:

GameManager.Instance

This means it can:

  • Hold state
  • Implement interfaces
  • Be referenced as an object
  • Work with Unity’s MonoBehaviour lifecycle

Use static classes for stateless utilities.

Use Singleton-like services when you genuinely need a shared instance.


Common Singleton Mistakes

Mistake 1 — Creating Multiple Instances

Always protect against duplicates.

Mistake 2 — Everything Is a Singleton

This creates global-state chaos.

Mistake 3 — Hidden Dependencies

A class may secretly depend on five different Singleton systems.

Mistake 4 — Incorrect Scene Management

Using DontDestroyOnLoad() everywhere can leave unwanted objects alive.

Mistake 5 — Testing Problems

Global state can make unit tests harder because one test can affect another.


A Practical Unity Architecture

For a small Unity project, you might have:

GameBootstrap
│
├── GameManager
├── AudioManager
├── SaveManager
└── SettingsManager

Then gameplay systems communicate with those services where appropriate.

For a larger project, consider moving toward:

Bootstrap
   │
   ├── Service Container
   │      ├── Audio Service
   │      ├── Save Service
   │      └── Analytics Service
   │
   └── Gameplay Systems
          ├── Player
          ├── Enemy
          ├── Inventory
          └── Quest

This gives you clearer separation of responsibilities.


Final Thoughts

The Singleton pattern isn’t inherently bad.

The real problem is using it for everything.

For a small Unity game, a Singleton can be a simple and effective solution for systems such as:

  • Audio
  • Save data
  • Game state
  • Settings
  • Configuration

As your project grows, however, consider alternatives such as Dependency Injection, interfaces, service containers, events, and composition.

The most important lesson is:

Use Singleton because the system genuinely needs one shared instance—not simply because Class.Instance is convenient.

Understanding when to use a pattern is more valuable than simply knowing how to implement it.

Leave a Comment