Skip to content
DasPeTe Tutorials

The Singleton Pattern in Unity

A generic singleton base class for managers that exist exactly once, a version that survives scene loads, and the traps to know about.

Quest objectives

  • Write a generic Singleton<T> base class for MonoBehaviours
  • Remove duplicates without breaking the instance
  • Keep a singleton across scene loads
  • Avoid the traps: Awake order, destroyed objects and Play mode without domain reload

A singleton is a class with exactly one instance, which the rest of the code reaches through a static property. In a game that suits managers that exist once and are needed everywhere: audio, saving, the game settings.

In Unity a singleton is usually a MonoBehaviour in the scene, so it can have fields in the Inspector and receive Unity's events. A generic base class saves you from writing the same lines into every manager:

Singleton.cs
using UnityEngine;

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

    protected virtual void Awake() {
        if (Instance != null && Instance != this) {
            // There is one already, so this one goes
            Destroy(gameObject);
            return;
        }

        Instance = (T)this;
    }

    protected virtual void OnDestroy() {
        if (Instance == this) {
            Instance = null;
        }
    }
}

where T : Singleton<T> says that T is the class that derives from it, as in AudioManager : Singleton<AudioManager>. That way Instance has the type of the manager itself, with all its methods, and the cast (T)this is allowed.

In Awake, a second instance destroys itself, for example one that comes with the next scene. The return matters: without it the duplicate would still take over Instance, which would then point to an object that is gone at the end of the frame.

OnDestroy clears Instance, but only if it is this object, so a duplicate on its way out doesn't take the real one with it.

The class is abstract because Unity can't add a generic component to an object anyway. A duplicate destroys its whole GameObject, so give every singleton a GameObject of its own.

Using it

A manager derives from the base class with its own name as T. If it needs Awake itself, it overrides it and calls base.Awake() first:

AudioManager.cs
using UnityEngine;

public class AudioManager : Singleton<AudioManager> {
    [SerializeField] AudioSource effects;

    protected override void Awake() {
        base.Awake();

        // A duplicate is on its way out and sets nothing up
        if (Instance != this) {
            return;
        }

        effects.volume = PlayerPrefs.GetFloat("effectsVolume", 1f);
    }

    public void PlayEffect(AudioClip clip) {
        effects.PlayOneShot(clip);
    }
}

After base.Awake() a duplicate is already on its way out, so it stops there before it sets anything up. Every other script reaches the manager through Instance:

Coin.cs
using UnityEngine;

public class Coin : MonoBehaviour {
    [SerializeField] AudioClip pickupSound;

    void OnTriggerEnter(Collider other) {
        if (!other.CompareTag("Player")) {
            return;
        }

        AudioManager.Instance.PlayEffect(pickupSound);
        Destroy(gameObject);
    }
}

Surviving scene loads

A singleton belongs to its scene and goes when the next scene loads. For a manager that should live as long as the game runs, a second base class adds DontDestroyOnLoad:

PersistentSingleton.cs
using UnityEngine;

public abstract class PersistentSingleton<T> : Singleton<T> where T : PersistentSingleton<T> {
    protected override void Awake() {
        base.Awake();

        if (Instance == this) {
            // Keep it when the next scene loads
            DontDestroyOnLoad(gameObject);
        }
    }
}

Put it into the first scene. If a later scene has one too, the check in Awake removes the newcomer and the first one stays. DontDestroyOnLoad only works for objects at the top of the hierarchy, which is one more reason to give a singleton a GameObject of its own.

Traps

  • Use Instance from Start on. Unity calls the Awake of different objects in no fixed order, so in the Awake of another object the singleton may not have set Instance yet. By Start, every Awake has run.

  • Destroyed objects. Instance != null uses Unity's own comparison, which also counts a destroyed object as null. Keep it that way and don't switch to is null or ??, which only see C# references.

  • Play mode without domain reload. Since Unity 6.6, new projects skip the domain reload when you enter Play mode, so static fields keep their values from the last session. Clearing Instance in OnDestroy takes care of that, because leaving Play mode destroys the objects of the scene. If you override OnDestroy, call base.OnDestroy().

Comments

The comments are GitHub Discussions: sign in with GitHub to write one.

The comments come from GitHub, through giscus. Loading them sends your IP address to giscus.app and GitHub.