Some Unity systems are naturally game-wide. Audio routing, scene transitions, save gateways, analytics clients, random number contexts, and platform bridges often need one authoritative runtime instance that survives scene loads. A persistent singleton is one way to express that requirement.
The pattern is not just “make a static variable.” A useful Unity implementation needs to handle scene objects, lazily created objects, play-mode behavior, duplicate prefabs, and initialization timing. The implementation below is a project-neutral version of a persistent MonoBehaviour singleton base class.
using UnityEngine;
public class PersistentSingleton<T> : MonoBehaviour where T : Component
{
[SerializeField] private bool unparentOnAwake = true;
public static bool HasInstance => s_instance != null;
public static T Current => s_instance;
protected static T s_instance;
public static T Instance
{
get
{
if (s_instance == null)
{
s_instance = FindFirstObjectByType<T>();
if (s_instance == null)
{
var obj = new GameObject($"{typeof(T).Name}");
s_instance = obj.AddComponent<T>();
}
}
return s_instance;
}
}
protected virtual void Awake()
{
InitializeSingleton();
}
private void InitializeSingleton()
{
if (!Application.isPlaying)
{
return;
}
if (unparentOnAwake)
{
transform.SetParent(null);
}
if (s_instance == null)
{
s_instance = this as T;
DontDestroyOnLoad(gameObject);
enabled = true;
OnSingletonInitialized();
}
else if (s_instance != this)
{
enabled = false;
gameObject.SetActive(false);
Destroy(gameObject);
}
else
{
OnSingletonInitialized();
}
}
protected virtual void OnSingletonInitialized()
{
}
}
The generic constraint where T : Component keeps the base class tied to Unity components. That matters because the singleton may need to find an existing component in the scene, add a component to a new GameObject, call DontDestroyOnLoad, or access transform.
The class exposes three static access points. HasInstance tells callers whether an instance has already been assigned. Current returns the current instance without creating anything. Instance is the creating accessor: if no instance exists, it searches the active scene first and creates a new object only when none is present.
Searching before creating is important. Many singleton services are configured as prefabs or scene objects with serialized fields, child objects, references, or editor-authored settings. If a configured instance already exists, Instance should adopt that object instead of replacing it with a blank runtime object. The lazy creation path is still useful for services that can initialize themselves entirely from code.
InitializeSingleton() handles the runtime ownership rules. It exits outside play mode so editor-time object inspection does not accidentally mutate singleton state. It optionally unparents the object before persistence because Unity only preserves a root GameObject cleanly with DontDestroyOnLoad. The first valid instance claims ownership, marks itself persistent, enables itself, and then runs OnSingletonInitialized().
Duplicate handling is just as important as creation. If another copy appears in a later scene, the base class disables it, hides it, and destroys it. This protects systems that must have one global event subscription, one audio output path, one save state coordinator, or one scene transition controller.
OnSingletonInitialized() is the extension point for subclasses. It runs after the component has become the authoritative instance. Use it for setup that belongs to the component itself, such as ensuring required Unity components exist, registering long-lived event listeners, or preparing internal state.
using UnityEngine;
[DisallowMultipleComponent]
public sealed class AudioManager : PersistentSingleton<AudioManager>
{
private AudioSource _source;
protected override void OnSingletonInitialized()
{
if (_source == null && !TryGetComponent(out _source))
{
_source = gameObject.AddComponent<AudioSource>();
}
}
public void ApplyConfiguration(AudioClip startupClip, bool loop)
{
_source.clip = startupClip;
_source.loop = loop;
}
public void PlayOneShot(AudioClip clip)
{
if (clip == null)
{
return;
}
_source.PlayOneShot(clip);
}
}
This keeps component setup near the component. A caller can request AudioManager.Instance, and the manager guarantees that its required AudioSource exists before public methods are expected to run.
However, singleton lifetime and singleton configuration are different problems. The base class can enforce “one persistent instance,” but it should not be responsible for all external data the service needs. A separate bootstrapper or installer can still configure the singleton explicitly:
using UnityEngine;
[CreateAssetMenu(menuName = "Installers/Audio Installer")]
public sealed class AudioInstaller : ScriptableObject
{
[SerializeField] private AudioClip startupClip;
[SerializeField] private bool loopStartupClip = true;
public void Install()
{
var audio = AudioManager.Instance;
audio.ApplyConfiguration(startupClip, loopStartupClip);
}
}
This separation keeps responsibilities clear. The singleton base class owns identity and lifetime. The derived service owns its internal setup. The installer or bootstrapper owns project-specific configuration and ordering.
The most common failure mode is initialization order. A service may exist before it is configured. For example, AudioManager.Instance can create the object and ensure an AudioSource, but an installer may still need to provide clips, mixer routing, volume defaults, or scene-specific rules. Consumers should not assume that Instance means “fully configured” unless the project has a clear bootstrap order that guarantees it.
A persistent singleton is therefore best treated as infrastructure, not architecture by itself. It is useful for global Unity services that need stable lifetime and one runtime owner. It becomes safer when paired with explicit configuration, narrow public APIs, and a bootstrap flow that makes initialization order visible.
In future posts, I will discuss ways to keep the convenience of persistent singletons while reducing the long-term cost of global access.