Every Unity project grows a GameManager. It starts as a tidy script that tracks the score, then it plays music, loads scenes, saves the game, holds the player reference and pauses time, and six months later nobody dares touch it. Singletons get the blame, but the real problem is usually how they’re built and how much they’re asked to do. This guide covers the Unity singleton pattern done safely in Unity 6, the bugs it causes when done badly, and three alternatives (a bootstrap scene, a service locator and ScriptableObject-based architecture) so you can pick the right level of structure for your game.
What a Game Manager Should (and Shouldn’t) Do
A “game manager” is really a place for things that exist once and outlive a single scene or object: audio, saving, scene loading, settings, input, analytics. The mistake is putting all of them in one class. A healthier structure is several small managers, each with one job:
AudioManager: music and sound playback.SaveManager: reading and writing save data.SceneLoader: transitions and loading screens.GameState: the current mode (menu, playing, paused, game over) and events when it changes.
What doesn’t belong in a global manager: anything tied to one level (the enemies in this room, this level’s timer), and references to scene objects that will be destroyed when the scene unloads. Those belong to scene-level controllers.
The Naive Singleton and Its Bugs
This version appears in countless tutorials:
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
void Awake()
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
It works in a one-scene prototype and breaks in four ways later:
- Duplicates. Return to the scene that contains the GameManager (the main menu, say) and a second copy is created. Both survive, both run, and the static field points at whichever woke last.
- Public writable field. Any script can overwrite
Instance. - Order problems. Another script’s
Awakemay run first and findInstancestill null. - Stale references with Enter Play Mode Options. If Domain Reload is disabled for faster play mode (a popular Unity 6 setting), static fields keep their old values between play sessions and point at destroyed objects.
A Safe MonoBehaviour Singleton
Here’s a version that fixes all four:
using UnityEngine;
public class AudioManager : MonoBehaviour
{
public static AudioManager Instance { get; private set; }
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject); // a copy already exists: remove this one
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
void OnDestroy()
{
if (Instance == this) Instance = null;
}
// Reset static state when Domain Reload is disabled
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetStatics() => Instance = null;
public void PlaySfx(AudioClip clip) { /* ... */ }
}
Key points:
- The property with a private setter stops other scripts replacing the instance.
- The duplicate check destroys any copy that arrives later, so scene reloads are safe.
DontDestroyOnLoadonly works on root GameObjects. If the manager is a child of another object, move it to the root or call it on the root.- The
SubsystemRegistrationmethod runs before anything else when entering Play mode, clearing statics even when Domain Reload is off. - Other scripts should access
InstanceinStartor later, notAwake, unless you control execution order (Project Settings > Script Execution Order, or the[DefaultExecutionOrder(-100)]attribute on the manager).
A Generic Singleton<T> Base Class
If you have several managers, a base class saves repeating that boilerplate:
using UnityEngine;
public abstract class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
public static T Instance { get; private set; }
[SerializeField] bool persistAcrossScenes = true;
protected virtual void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = (T)this;
if (persistAcrossScenes)
{
transform.SetParent(null);
DontDestroyOnLoad(gameObject);
}
}
protected virtual void OnDestroy()
{
if (Instance == this) Instance = null;
}
}
public class SaveManager : Singleton<SaveManager>
{
protected override void Awake()
{
base.Awake();
if (Instance != this) return; // this copy is being destroyed
// your setup here
}
}
Generic classes can’t use RuntimeInitializeOnLoadMethod for every T, so if you disable Domain Reload, add a small static reset method in each concrete manager, or use the bootstrap approach below, which avoids the problem.
Avoid “lazy” singletons that create themselves with new GameObject() when Instance is first accessed. They hide setup order bugs and can spawn ghost managers while the application is quitting (the classic “Some objects were not cleaned up when closing the scene” warning).
Bootstrapping Managers Automatically
The most robust way to guarantee managers exist, no matter which scene you press Play in, is to create them before the first scene loads:
using UnityEngine;
public static class Bootstrapper
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void Init()
{
var prefab = Resources.Load<GameObject>("Systems");
Object.DontDestroyOnLoad(Object.Instantiate(prefab));
}
}
Put a Systems prefab in a Resources folder containing all your managers as children of one root (each manager then doesn’t need its own DontDestroyOnLoad). Now every scene works when played directly, managers always exist before any scene’s Awake, and there are never duplicates because no scene contains them.
The other common pattern is a dedicated bootstrap scene at build index 0 that holds the managers and loads the first real scene additively. It’s very clear and works well with loading screens; see our async scene loading guide. Its weakness is that pressing Play in a level scene skips the bootstrap, unless you add an Editor script that redirects Play mode to scene 0.
The Service Locator Pattern
Singletons couple every caller to a concrete class: AudioManager.Instance.PlaySfx() means you can’t swap in a silent version for tests or a different implementation per platform. A service locator registers services by interface:
using System;
using System.Collections.Generic;
public static class Services
{
static readonly Dictionary<Type, object> map = new();
public static void Register<T>(T service) where T : class => map[typeof(T)] = service;
public static void Unregister<T>() where T : class => map.Remove(typeof(T));
public static T Get<T>() where T : class =>
map.TryGetValue(typeof(T), out var s) ? (T)s
: throw new InvalidOperationException($"Service {typeof(T).Name} not registered");
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
static void Reset() => map.Clear();
}
public interface IAudioService { void PlaySfx(UnityEngine.AudioClip clip); }
public class AudioManager : UnityEngine.MonoBehaviour, IAudioService
{
void Awake() => Services.Register<IAudioService>(this);
void OnDestroy() => Services.Unregister<IAudioService>();
public void PlaySfx(UnityEngine.AudioClip clip) { /* ... */ }
}
// usage
Services.Get<IAudioService>().PlaySfx(jumpClip);
It’s still global access, but callers depend on an interface, and you can register a fake in tests. For bigger teams, dependency injection frameworks such as VContainer or Extenject go further by passing services into constructors and fields automatically.
📚 Go deeper on game architecture
Game Programming Patterns explains singletons, service locators and event queues brilliantly, and it’s free to read online. It and other favourites are on our reading list.
ScriptableObject Architecture
Another way to share state without singletons is to make the data an asset. A ScriptableObject lives in the project, any object can reference it in the Inspector, and it exists independently of scenes:
[CreateAssetMenu(menuName = "Game/Int Variable")]
public class IntVariable : ScriptableObject
{
public int Value;
public event System.Action<int> Changed;
public void Set(int v) { Value = v; Changed?.Invoke(v); }
}
The player script holds a reference to a PlayerScore asset and calls Set; the UI references the same asset and listens to Changed. Neither knows the other exists, there’s no singleton, and designers can wire things up in the Inspector. The same idea works for event channels (a ScriptableObject that raises an event, such as OnPlayerDied).
Two cautions: values changed in Play mode persist in the asset in the Editor (reset them on start), and large projects can drown in tiny assets. Our ScriptableObjects guide covers these patterns in detail, and C# events and UnityEvents explains the event side.
Which Approach Should You Use?
| Approach | Best for | Main weakness |
|---|---|---|
| Safe singleton | Small and medium games, solo developers | Tight coupling; hard to test |
| Bootstrapped systems prefab | Any project with several managers | Resources folder or a bootstrap scene to maintain |
| Service locator | Projects wanting interfaces and testability | Missing services fail at runtime, not compile time |
| ScriptableObject variables and events | Designer-driven projects, decoupled UI | Many small assets; Editor persistence surprises |
| DI framework (VContainer, Extenject) | Larger teams and long-lived codebases | Learning curve and setup |
A pragmatic combination for most indie games: a bootstrapped systems prefab containing a few focused singletons, plus C# events or ScriptableObject event channels so gameplay objects don’t reach into managers directly.
Troubleshooting
NullReferenceException when accessing Instance
The manager isn’t in the scene you pressed Play in, or your script accesses it in Awake before the manager’s Awake ran. Bootstrap managers before scene load, or access them in Start. Our NullReferenceException guide covers the general case.
Two GameManagers exist after returning to the menu
The singleton has no duplicate check. Add the Instance != this destroy logic, or remove managers from scenes entirely and bootstrap them.
“DontDestroyOnLoad only works for root GameObjects or components on root GameObjects”
The manager is a child of another object. Unparent it with transform.SetParent(null) before calling DontDestroyOnLoad.
Old values persist between play sessions
Domain Reload is disabled in Enter Play Mode Options, so statics aren’t reset. Add RuntimeInitializeOnLoadMethod(SubsystemRegistration) reset methods for every static field and event.
Manager references to scene objects become “Missing” after loading a level
Persistent managers shouldn’t hold references to objects in a scene that gets unloaded. Have scene objects register with the manager on enable and unregister on disable instead.
FAQ
Are singletons bad in Unity?
Not inherently. A few well-built singletons for truly global systems are fine. Problems come from duplicates, initialisation order and putting too much into one manager.
How do I make a GameManager persist between scenes?
Call DontDestroyOnLoad on its root GameObject and add a duplicate check, or create it before the first scene with RuntimeInitializeOnLoadMethod(BeforeSceneLoad).
What is a service locator in Unity?
A static registry where services register by interface and others look them up, giving global access without depending on concrete singleton classes.
Should I use ScriptableObjects instead of singletons?
For shared data and events, ScriptableObjects work well and decouple systems. For services that need Update, coroutines or scene presence, a MonoBehaviour manager is still simpler.
Why is my singleton null in Awake?
Awake order between scripts isn’t guaranteed. Access other managers in Start, or give the manager an earlier execution order with DefaultExecutionOrder.
Do I need to reset statics in Unity 6?
Only if Domain Reload is disabled in Enter Play Mode Options. Then static fields keep values between play sessions and must be reset manually.
Reader questions
Asked by readers of this article and answered by AI. Behavior can differ between Unity versions, so test in yours.



Leave a Reply