The Singleton pattern ensures a class has only one instance throughout a application's lifecycle and provides a global access point to it. This design is beneficial when multiple operations must coordinate with a shared resource, such as a file system manager where concurrent create and delete actions could conflict if separate instances were unaware of each other.
Two primary implementation approaches exist:
Lazy Initialization The instance is created only when first requested.
using UnityEngine;
public class LazySingleton : MonoBehaviour
{
private static LazySingleton _sharedInstance;
private static readonly object _lockHandle = new object();
public static LazySingleton Shared
{
get
{
if (_sharedInstance == null)
{
lock (_lockHandle)
{
if (_sharedInstance == null)
{
GameObject container = new GameObject("SingletonContainer");
_sharedInstance = container.AddComponent<LazySingleton>();
}
}
}
return _sharedInstance;
}
}
}
The double-checked locking pattern shown addresses thread safety concerns in multi-threaded environments while minimizing performance overhead. The outer null check avoids unnecessary locking once the instance exists.
Eager Initialization The instance is created during class initialization.
using UnityEngine;
public class EagerSingleton : MonoBehaviour
{
private static EagerSingleton _globalInstance;
public static EagerSingleton Global
{
get { return _globalInstance; }
}
private void Awake()
{
if (_globalInstance == null)
{
_globalInstance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
When attaching this component to multiple GameObjects, only the first created instance persists; subsequent duplicates self-destruct during Awake().
Generic Singleton Base Class A reusable implemantation for MonoBehaviour-derived classes:
using UnityEngine;
public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _primaryInstance;
public static T Primary
{
get { return _primaryInstance; }
}
protected virtual void OnEnable()
{
if (_primaryInstance == null)
{
_primaryInstance = this as T;
}
else
{
Destroy(this);
}
}
}
Derived classes inherit singleton behavior while allowing custom Awake/Start logic through method overriding.
Design Considerations
Memory vs. Performance: Lazy initialization conserves memory by deferring object creation until needed, while eager initialization eliminates runtime instantiation overhead.
Initialization Timing: Lazy initialization can cause performance spikes if instantiation occurs during critical gameplay moments. Eager initialization ensures resources are loaded during predictable startup phases.
Global State Management: Singleton accessibility can obscure dependencies and increase coupling between systems. Consider dependency injection alternatives when testability and modularity are prioriteis.
Thread Safety: The double-checked locking pattern provides thread-safe lazy initialization without constant synchronization overhead after instance creation.
Unity-Specific Constraints: MonoBehaviour-derived singletons require GameObject instantiation rather than direct constructor calls. The DontDestroyOnLoad() method preserves instances across scene transitions.