Skip to main content

Singleton Patterns

QWR Utilities provides several singleton implementations to handle different use cases in Unity development. This page explains how to use and customize these singleton patterns.

Singleton Overview

Singletons ensure that a class has only one instance and provide a global point of access to that instance. QWR Utilities offers several singleton implementations:

  • MonoBehaviourSingleton<T>: Base class for MonoBehaviour singletons
  • ScriptableSingleton<T>: Base class for ScriptableObject singletons
  • PersistentSingleton<T>: Singleton that persists between scene loads
  • LazyStaticSingleton<T>: Non-MonoBehaviour singleton with lazy initialization

MonoBehaviourSingleton

The MonoBehaviourSingleton<T> is a base class for creating singleton MonoBehaviours.

Basic Usage

using QWR.Utilities;
using UnityEngine;

public class GameManager : MonoBehaviourSingleton<GameManager>
{
public int score = 0;

protected override void Awake()
{
base.Awake(); // Important: Always call base.Awake()

// Your initialization code
Debug.Log("GameManager initialized");
}

public void AddScore(int points)
{
score += points;
Debug.Log($"Score: {score}");
}
}

Accessing the Singleton

// Access the singleton instance from anywhere
GameManager.Instance.AddScore(10);

// Check if instance exists
if (GameManager.HasInstance)
{
GameManager.Instance.AddScore(5);
}

Customizing Instance Handling

You can customize how duplicate instances are handled:

public class CustomManager : MonoBehaviourSingleton<CustomManager>
{
// Override to customize instance handling
protected override bool DestroyDuplicateInstances => true;

// Override to customize DontDestroyOnLoad behavior
protected override bool DontDestroyOnLoadInstance => false;

protected override void OnDuplicateInstanceFound(CustomManager instance)
{
// Custom handling when a duplicate instance is found
Debug.LogWarning($"Duplicate instance found: {instance.name}");

// Call base implementation to destroy or handle the duplicate
base.OnDuplicateInstanceFound(instance);
}
}

ScriptableSingleton

The ScriptableSingleton<T> is a base class for creating singleton ScriptableObjects, which is useful for configuration data.

Creating a ScriptableSingleton

using QWR.Utilities;
using UnityEngine;

[CreateAssetMenu(fileName = "GameSettings", menuName = "QWR/Game Settings")]
public class GameSettings : ScriptableSingleton<GameSettings>
{
public int difficulty = 1;
public float musicVolume = 0.8f;
public float sfxVolume = 1.0f;

// Optional: Override to specify a custom resource path
protected override string ResourcePath => "Settings/GameSettings";
}

Accessing ScriptableSingleton

// Access the singleton instance
float volume = GameSettings.Instance.musicVolume;

// Modify values
GameSettings.Instance.difficulty = 2;

// Check if instance exists
if (GameSettings.HasInstance)
{
Debug.Log($"Difficulty: {GameSettings.Instance.difficulty}");
}

Creating the Asset

For ScriptableSingletons, you need to create the asset in your project:

  1. Right-click in the Project window
  2. Select "Create > QWR > Game Settings"
  3. Name the asset (preferably matching the ResourcePath)
  4. Place it in a Resources folder (or the custom path you specified)

PersistentSingleton

The PersistentSingleton<T> is a MonoBehaviour singleton that persists between scene loads.

Creating a PersistentSingleton

using QWR.Utilities;
using UnityEngine;

public class SaveManager : PersistentSingleton<SaveManager>
{
private GameData gameData;

protected override void Awake()
{
base.Awake(); // Important: Always call base.Awake()

// This manager will persist between scenes
Debug.Log("SaveManager initialized and will persist between scenes");

// Load data
LoadGameData();
}

public void SaveGameData()
{
// Save game data implementation
}

private void LoadGameData()
{
// Load game data implementation
}
}

Usage Considerations

PersistentSingletons are automatically marked with DontDestroyOnLoad, so they persist across scene changes:

// Access the persistent singleton
SaveManager.Instance.SaveGameData();

// The instance will still be available after loading a new scene
SceneManager.LoadScene("Level2");
// Later, in Level2 scene
SaveManager.Instance.SaveGameData(); // Same instance

LazyStaticSingleton

The LazyStaticSingleton<T> is a non-MonoBehaviour singleton with lazy initialization.

Creating a LazyStaticSingleton

using QWR.Utilities;

public class ConfigManager : LazyStaticSingleton<ConfigManager>
{
private Dictionary<string, object> configValues = new Dictionary<string, object>();

// Constructor is called only when Instance is first accessed
public ConfigManager()
{
LoadConfigValues();
}

public T GetValue<T>(string key, T defaultValue = default)
{
if (configValues.TryGetValue(key, out object value) && value is T typedValue)
{
return typedValue;
}
return defaultValue;
}

public void SetValue<T>(string key, T value)
{
configValues[key] = value;
}

private void LoadConfigValues()
{
// Load configuration values implementation
}
}

Accessing LazyStaticSingleton

// First access will initialize the singleton
int maxPlayers = ConfigManager.Instance.GetValue<int>("MaxPlayers", 4);

// Set a configuration value
ConfigManager.Instance.SetValue("Difficulty", 2);

Advanced Singleton Patterns

Generic Singleton with Constraints

You can create a more constrained singleton by using generic constraints:

using QWR.Utilities;
using UnityEngine;

// Only classes implementing IManager can use this singleton
public class ConstrainedSingleton<T> : MonoBehaviourSingleton<T> where T : MonoBehaviour, IManager
{
public void ExecuteManagerFunction()
{
// Call interface method
(this as IManager).ManageSystem();
}
}

// Interface for managers
public interface IManager
{
void ManageSystem();
}

// Implementation
public class NetworkManager : ConstrainedSingleton<NetworkManager>, IManager
{
public void ManageSystem()
{
Debug.Log("Managing network system");
}
}

Auto-Creating Singleton

You can create a singleton that automatically creates itself if it doesn't exist:

using QWR.Utilities;
using UnityEngine;

public class AutoCreatingSingleton<T> : MonoBehaviourSingleton<T> where T : MonoBehaviour
{
// Override the accessor to auto-create if needed
new public static T Instance
{
get
{
if (!HasInstance)
{
// Create a new GameObject and add the component
GameObject obj = new GameObject(typeof(T).Name);
_instance = obj.AddComponent<T>();
}
return _instance;
}
}
}

// Implementation
public class UIManager : AutoCreatingSingleton<UIManager>
{
// UI Manager implementation
}

Best Practices

When to Use Singletons

Singletons are best used for:

  • Global managers that need to be accessed from many places
  • Systems that should have only one instance by design
  • Configuration or settings that need global access

When to Avoid Singletons

Consider alternatives when:

  • The class doesn't truly need to be a singleton
  • You want better testability (singletons can make testing difficult)
  • You need multiple instances of the class
  • You're creating tight coupling between classes

Singleton Design Tips

  • Always call base.Awake() when overriding the Awake method
  • Keep singleton responsibilities focused and specific
  • Consider using dependency injection alongside singletons
  • Document singleton behavior clearly, especially for team projects

Common Pitfalls

Initialization Order

Be careful with initialization order when using multiple singletons:

// Potential issue: Depends on AudioManager being initialized first
public class MusicManager : MonoBehaviourSingleton<MusicManager>
{
protected override void Awake()
{
base.Awake();

// This might fail if AudioManager isn't initialized yet
AudioManager.Instance.SetVolume(0.5f);
}

// Better approach: Use Start or explicit initialization
private void Start()
{
// By Start, all Awake methods should have run
AudioManager.Instance.SetVolume(0.5f);
}
}

Scene Loading Issues

Be aware of scene loading behavior:

  • MonoBehaviourSingleton<T> instances are destroyed on scene load unless marked with DontDestroyOnLoad
  • PersistentSingleton<T> always uses DontDestroyOnLoad
  • ScriptableSingleton<T> persists naturally as ScriptableObjects aren't scene-dependent

Thread Safety

The singleton implementations in QWR Utilities are not thread-safe by default. If you need thread safety:

using QWR.Utilities;
using System;
using UnityEngine;

public class ThreadSafeSingleton<T> : MonoBehaviourSingleton<T> where T : MonoBehaviour
{
private static readonly object _lock = new object();

new public static T Instance
{
get
{
lock (_lock)
{
return MonoBehaviourSingleton<T>.Instance;
}
}
}
}

Next Steps

Now that you understand singleton patterns, you can explore: