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 singletonsScriptableSingleton<T>: Base class for ScriptableObject singletonsPersistentSingleton<T>: Singleton that persists between scene loadsLazyStaticSingleton<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:
- Right-click in the Project window
- Select "Create > QWR > Game Settings"
- Name the asset (preferably matching the ResourcePath)
- 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 withDontDestroyOnLoadPersistentSingleton<T>always usesDontDestroyOnLoadScriptableSingleton<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:
- Logging: Learn about the enhanced logging system
- Extension Methods: Discover useful extensions for Unity objects
- Data Structures: Explore specialized data structures