Designing Singletons in Unity: One Pattern, Three Lifetimes
Why every game ends up with a few global objects, how Unity's rules reshape the singleton design, the three shapes it takes, and what global state costs you.
Every game ends up with the same handful of objects. Something that plays sounds. Something that saves progress. Something that decides which scene comes next. None of them should ever exist twice, and half the codebase wants to talk to them. In a normal app this is where you’d reach for a global variable. In Unity you reach for a singleton, mostly because the engine doesn’t hand you anything else that feels global.
The textbook version of the pattern is small. Hide the constructor, keep one static instance, build it the first time someone asks:
public class GameManager
{
private static GameManager instance;
private static readonly object gate = new object();
public static GameManager Instance
{
get
{
if (instance == null)
{
lock (gate)
{
if (instance == null)
instance = new GameManager();
}
}
return instance;
}
}
private GameManager() { }
}
The lock dance looks paranoid, but it’s the standard double-check: the first null test keeps the fast path lock-free, the second makes sure two threads don’t each build their own instance. For a game loop that runs on one thread this feels like overkill. It is — until a loading task on a background thread touches Instance and it isn’t.
So far, so good. Now try making GameManager a MonoBehaviour, and the whole thing falls apart. Not because of the pattern. Because of the engine.
You can’t write new T() anymore, to start. The engine owns construction: a MonoBehaviour has to sit on a GameObject and be built by Unity itself. The lazy-creation idea doesn’t die, but it has to be rebuilt out of engine parts.
Scenes kill objects while statics survive them. Your static field keeps pointing at something the scene unloaded. The fun part is that Unity overloads ==, so a destroyed object compares equal to null even though a real, dead reference is still sitting there. People complain about this fake null constantly, yet singleton code quietly lives off it — if (instance == null) becomes both a way to notice the decay and a way to rebuild after it.
Meanwhile, almost nothing in Unity is thread-safe, so the deeper you go into engine types the less the lock means. Plain C# singletons still want it. MonoBehaviour ones basically never do.
And one more that bites late: statics reset when the domain reloads, but you can switch that reload off when entering Play Mode. Do that, and last session’s singleton walks into this one still wearing yesterday’s state. “It’s a fresh run” is something you have to earn, not assume.
Three shapes, one question
What you end up with — what I think everyone ends up with — is three shapes of the same idea. The difference between them is a single question: how long should this thing be allowed to live?
The plain one
If a class doesn’t need Update, coroutines, or inspector fields, don’t drag it through MonoBehaviour at all. Keep the classic design, but make it reusable — one base class, and every future singleton comes almost for free:
public abstract class Singleton<T> where T : Singleton<T>, new()
{
private static T instance;
private static readonly object gate = new object();
private bool initialized;
public static T Instance
{
get
{
if (instance == null)
{
lock (gate)
{
if (instance == null)
{
instance = new T();
instance.Initialize();
}
}
}
return instance;
}
}
public void Initialize()
{
if (initialized) return; // run once, ever
initialized = true;
OnInitializing();
OnInitialized();
}
protected virtual void OnInitializing() { }
protected virtual void OnInitialized() { }
protected virtual void Clear() { }
public static void Destroy()
{
instance?.Clear();
instance = null;
}
}
// a whole new singleton, one line:
public class SaveSystem : Singleton<SaveSystem> { }
That constraint — where T : Singleton<T> — looks like a typo the first ten times you see it. It means every subclass has to name itself as the generic argument, which is exactly what lets the base class hand back the right type from Instance and construct it with new T(). The little initialized flag turns setup into a state machine, so the hooks run exactly once even if two threads race. And Clear matters more than it looks: it’s the reset path for tests, and for any “start a new run” feature that wants the old state gone.
The one that lives in a scene
Some things genuinely need the engine — coroutines, physics, inspector-tuned fields. Those have to be MonoBehaviours on real GameObjects, and now the problem changes: someone might have already placed the object in the scene by hand. So Instance has to handle three situations. Nothing exists yet. Something exists and is registered. Or something exists but nobody registered it.
public abstract class MonoSingleton<T> : MonoBehaviour
where T : MonoSingleton<T>
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
instance = FindObjectOfType<T>(); // already in the scene?
if (instance == null)
{
var go = new GameObject(typeof(T).Name);
instance = go.AddComponent<T>(); // build one ourselves
}
return instance;
}
}
protected virtual void Awake()
{
if (instance == null)
instance = this as T; // first one in wins
else if (instance != this)
Destroy(gameObject); // a duplicate showed up; evict it
}
}
The trick in Awake is my favourite part of the whole pattern. The first object claims the static slot. Any latecomer — a copy someone dragged in, a prefab loaded twice — finds the slot taken and destroys itself. You never write “don’t place two of these” checks at call sites; the objects police each other.
The getter does the polite thing first: look around the scene before building anything, so hand-placed objects get respected. Only when nothing exists does it spawn a GameObject and add the component — that’s the Unity-native version of new T().
One trap hides here, and everyone hits it once: script execution order. If another script reads Instance inside its own Awake, the lookup runs before this class has registered anything. Either the scene search finds the object anyway and all is well, or you pin this script to run earlier — Unity’s execution-order settings, or a [DefaultExecutionOrder] attribute on the class — so registration wins the race.
The one that outlives the scene
The scene version dies with its scene. Audio and save systems shouldn’t. The fix is small enough that people are tempted to copy-paste the previous class and add a line. Resist that; extend it instead:
public abstract class PersistentMonoSingleton<T> : MonoSingleton<T>
where T : MonoSingleton<T>
{
[SerializeField] private bool unparentOnAwake = true;
protected override void Awake()
{
if (unparentOnAwake) transform.SetParent(null);
base.Awake(); // claim or evict, as before
if (instance == this)
DontDestroyOnLoad(gameObject); // the survivor gets to stay
}
}
Two defensive touches. DontDestroyOnLoad moves the object into the engine’s keep-alive scene, which is the entire point. The unparenting looks weird until you’ve seen it happen: someone parents the manager under some UI panel, that panel dies with the scene, and your “persistent” object goes with it. Refusing bad hierarchies beats failing silently later. And the instance == this check makes sure only the survivor stays — duplicates being evicted don’t get persistence as a consolation prize.
One habit worth borrowing: keep all of these in a tiny bootstrap scene that loads first, and let every other scene assume it’s already there. The pattern guarantees there will be exactly one. The bootstrap guarantees it’ll be there early.
What it costs
None of this is free, and the bill shows up later as architecture.
Instance works from literally anywhere, which is the selling point and also the problem. Dependencies stop being visible — a constructor announces what a class needs, while an Instance call quietly grabs whatever it likes at runtime. When the most central logic in the game is also the hardest to trace, refactoring gets interesting.
Tests suffer next. Static instances leak state between tests and won’t accept a stand-in. That’s why the plain version grew a reset hook; without one, your test results start depending on the order you run them in.
Then there’s my least favourite bug, the one everyone writes exactly once. The lazy getter will happily build a new instance during teardown. Something touches Instance in OnDestroy — during scene unload, after the real one is already being destroyed — and a ghost gets born. It survives the unload, and the next scene inherits a zombie. Lazy is convenient right up to the moment it isn’t.
There are alternatives, and a growing project should reach for them: constructor injection, a service locator that at least keeps lookup in one place, or ScriptableObjects as engine-native shared state. All of them cost some ceremony, which is exactly what the singleton doesn’t have. So the honest rule of thumb I’ve settled on: a singleton for things that are truly one-of-a-kind, wanted everywhere, and stable for the whole session — audio, save, scene flow. The moment it turns into a bag of loose state, it’s a global variable with extra steps.
In the end, the three shapes answer one question: how long should this instance live? The process, for plain classes. The scene, for things wired into the engine. The whole session, for the persistent version stacked on top. The pattern itself was never the problem. Letting “exactly one” quietly become “exactly one dump for everything” is.