Overview
When developing games in Unity, you inevitably encounter situations where you need "a single manager accessible from anywhere." For example, a class managing overall game score, a class controlling BGM and sound effects, or a class holding game state (paused, game over, etc.).
Finding these manager classes with GameObject.Find() every time or manually setting references from the Inspector is very tedious. Also, accidentally placing multiple instances of the same manager class across scenes can cause unexpected bugs.
This article explains the basic concepts of the Singleton pattern and how to implement it safely in Unity.
Basic Concepts of the Singleton Pattern
The Singleton pattern is a mechanism that guarantees a class's instance is unique throughout the application and provides a global access point to that instance.
To achieve this in a regular C# class, the following elements are typically needed:
- Static instance variable: A
staticvariable to hold the class itself. - Private constructor: To prevent external instantiation via
new. - Static access property: A
staticproperty to return the unique instance.
Implementing MonoBehaviour Singleton in Unity
When implementing the Singleton pattern in Unity, many manager classes need to inherit from MonoBehaviour. This is to use Unity's lifecycle methods like Awake and Update, or to configure from the Inspector.
Here we introduce a method to create a versatile base class using Generics that can make any class a Singleton. This method is considered one of the best practices for Singleton implementation in Unity.
Generic MonoSingleton Class
Save the following code as MonoSingleton.cs.
using UnityEngine;
// Constraint that T must be a class inheriting MonoBehaviour
public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T>
{
// Static variable holding the unique instance
private static T instance;
// Static property for external access
public static T Instance
{
get
{
// If instance doesn't exist yet
if (instance == null)
{
// Find T type object in scene
instance = (T)FindObjectOfType(typeof(T));
// If still not found, create new GameObject and attach
if (instance == null)
{
GameObject singletonObject = new GameObject();
instance = singletonObject.AddComponent<T>();
singletonObject.name = typeof(T).ToString() + " (Singleton)";
}
}
return instance;
}
}
// Called immediately after instance is created
protected virtual void Awake()
{
// If instance already exists (duplicate prevention)
if (instance != null && instance != this)
{
// Destroy self to prevent duplicates
Debug.LogWarning($"[Singleton] Instance already exists, destroying {typeof(T).Name}.");
Destroy(gameObject);
return;
}
// Set self as the unique instance
instance = (T)this;
// Persist object across scene transitions
// Only apply at runtime due to editor behavior considerations
if (Application.isPlaying)
{
DontDestroyOnLoad(gameObject);
}
// Method to delegate initialization to child classes
OnInitialize();
}
// Clear static reference on destruction
protected virtual void OnDestroy()
{
if (instance == this)
{
instance = null;
}
}
// Method for child classes to override for initialization
protected virtual void OnInitialize() { }
}
Usage (Example: Sound Manager)
Next, create an actual manager class by inheriting this base class.
using UnityEngine;
// Inherit MonoSingleton<SoundManager>
public class SoundManager : MonoSingleton<SoundManager>
{
// Public methods for external access
public void PlayBGM(AudioClip clip)
{
Debug.Log($"Playing BGM: {clip.name}");
// Actual BGM playback logic...
}
public void PlaySE(AudioClip clip)
{
Debug.Log($"Playing sound effect: {clip.name}");
// Actual SE playback logic...
}
// Override initialization process
protected override void OnInitialize()
{
// Processes to execute only once at initialization, like loading sound settings
Debug.Log("SoundManager initialization complete.");
}
}
Access Method
Now you can easily access it from anywhere in the game, regardless of scene.
// From some other script
public class PlayerController : MonoBehaviour
{
public AudioClip jumpSound;
void Jump()
{
// Access the unique instance and call method
SoundManager.Instance.PlaySE(jumpSound);
}
}
Key Point: The MonoSingleton<T> class's Instance property automatically searches the scene if the instance doesn't exist, and creates a new GameObject to attach if still not found. This prevents errors even if you forget to manually place it in the scene, increasing safety.
3 Common Pitfalls for Beginners and Solutions
While the Singleton pattern is convenient, Unity-specific lifecycle issues can trap beginners.
1. Duplicate Instance Creation During Scene Transitions
The most common problem is that a Singleton class using DontDestroyOnLoad gets duplicated when moving between scenes.
- Mistake Example:
SoundManageris created in Scene A and persisted withDontDestroyOnLoad. When Scene B loads, if Scene B also has aSoundManagerprefab placed, a second instance is created. - Solution: As in the
MonoSingleton<T>implementation above, check ifinstancealready exists in theAwakemethod, and if so, immediately destroy self withDestroy(gameObject);. This guarantees only one instance always exists.
2. Reference Order Issues (Script Execution Order)
In Unity, script execution order (when Awake and Start are called) is basically undefined. When trying to access a Singleton's Instance in some script's Awake, that Singleton's Awake may not have executed yet, and instance may be null.
- Solutions:
- By adopting logic that creates instance within the
Instanceproperty (like the code above), the instance is guaranteed to exist when accessed. - In Script Execution Order settings (
Edit->Project Settings->Script Execution Order), set Singleton classes to execute earlier than other scripts. This guarantees initialization completes before other scripts access them.
- By adopting logic that creates instance within the
3. Forgetting to Clear Static Reference in OnDestroy
During scene transitions or game exit, Singleton objects may be destroyed. If you forget to clear the static instance variable at this time, a reference to an already destroyed object remains, potentially causing unexpected errors (MissingReferenceException etc.) on next access.
- Solution: Implement
OnDestroymethod in theMonoSingleton<T>class to clear the static reference withinstance = null;when the object is destroyed.
// Clear static reference on destruction
protected virtual void OnDestroy()
{
if (instance == this)
{
instance = null;
}
}
Singleton Pattern Caveats (Disadvantages)
While the Singleton pattern is very powerful, overuse can harm project health. Even small-scale developers should know these two main disadvantages:
-
Tight Coupling (Strong Dependencies): Since Singletons provide global access points, any class can easily access them. As a result, many classes become dependent on specific Singleton classes, increasing code coupling. Tight coupling increases the risk of unexpected bugs when modifying parts of the code.
-
Testing Difficulty: When performing unit tests, Singleton classes hold global state, making it difficult to maintain test independence as state is shared between tests. Also, replacing Singleton classes with mocks (fake objects) is difficult, reducing test flexibility.
Summary
This article explained the correct implementation and caveats of the Singleton pattern in Unity development. Proper use of this pattern keeps your game's management structure simple.
Key takeaways:
- The Singleton pattern is a design pattern that guarantees a single instance and provides global access.
- In Unity, creating a generic base class inheriting
MonoBehaviouris the best practice for safe and versatile Singleton implementation. - Performing duplicate instance check and destruction in the
Awakemethod prevents bugs during scene transitions. - Applying
DontDestroyOnLoad(gameObject);persists objects across scenes. - Singleton overuse causes tight coupling, harming code maintainability and testability, so limiting use to global managers is important.
Use this knowledge to make your Unity projects more efficient and robust.