Skip to content

Instantly share code, notes, and snippets.

@TitanX101
Last active January 24, 2026 21:52
Show Gist options
  • Select an option

  • Save TitanX101/7431535751f5442ceb8046a365ec49d0 to your computer and use it in GitHub Desktop.

Select an option

Save TitanX101/7431535751f5442ceb8046a365ec49d0 to your computer and use it in GitHub Desktop.
Generic Singleton MonoBehaviour for Unity with optional auto-load via Resources or Addressables, supporting persistent instances and async retrieval.
using System;
using UnityEngine;
#if UNITY_ADDRESSABLES
using UnityEngine.AddressableAssets;
#endif
namespace Empress {
/// <summary>
/// Attribute used to mark a singleton class that should be auto-loaded.
/// Supports optional configuration for Resources path and persistence.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class SingletonAutoLoadAttribute : Attribute {
/// <summary>
/// Path in Resources to load the singleton asset.
/// </summary>
public string ResourcePath { get; set; }
#if UNITY_ADDRESSABLES
/// <summary>
/// Optional addressable key to load the singleton asset via <see cref="Addressables"/>.
/// </summary>
public string AddressableKey { get; set; }
#endif
/// <summary>
/// Indicates whether the singleton should be persistent between play sessions.
/// Relevant for <see cref="MonoBehaviour"/> singletons, ignored for <see cref="ScriptableObject"/>.
/// </summary>
public bool IsPersistent { get; set; }
}
}
using System;
using System.IO;
using System.Reflection;
using UnityEngine;
#if UNITY_ADDRESSABLES
using System.Threading.Tasks;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
#endif
namespace Empress {
/// <summary>
/// Base type used to identify singleton behaviours without generics.
/// </summary>
public abstract class SingletonBehaviour : MonoBehaviour { }
/// <summary>
/// Generic singleton behaviour base class.
/// Ensures a single active instance of <typeparamref name="T"/> exists.
/// </summary>
/// <typeparam name="T">MonoBehaviour type to be used as singleton.</typeparam>
public abstract class SingletonBehaviour<T> : SingletonBehaviour where T : MonoBehaviour {
#region PROPERTIES
/// <summary>Cached type of the singleton.</summary>
public static readonly Type Type = typeof(T);
/// <summary>Returns true if this singleton has an auto-load attribute.</summary>
public static bool IsAutoLoad => AutoLoad != null;
/// <summary>Auto-load configuration from <see cref="SingletonAutoLoadAttribute"/>.</summary>
public static SingletonAutoLoadAttribute AutoLoad { get; } = Type.GetCustomAttribute<SingletonAutoLoadAttribute>();
/// <summary>
/// Returns the singleton instance of type <typeparamref name="T"/>.
/// Automatically finds or creates the instance if necessary.
/// </summary>
public static T Instance {
get {
#if UNITY_EDITOR
if (!Application.isPlaying)
return null;
#endif
if (m_Instance != null)
return m_Instance;
// Try to find in scene
m_Instance = FindFirstObjectByType<T>();
// Auto-load from Resources or Addressables
if (m_Instance == null && IsAutoLoad) {
T singletonPrefab = null;
// Load via Resources
if (!string.IsNullOrEmpty(AutoLoad.ResourcePath)) {
singletonPrefab = Resources.Load<T>(AutoLoad.ResourcePath);
if (singletonPrefab == null)
singletonPrefab = Resources.Load<T>(Path.Combine(AutoLoad.ResourcePath, Type.Name));
}
#if UNITY_ADDRESSABLES
// Load via Addressables
else if (!string.IsNullOrEmpty(AutoLoad.AddressableKey)) {
#if UNITY_WEBGL
Debug.LogError($"[{Type.Name}] Synchronous Addressables loading is not supported in WebGL. Use GetInstanceAsync() instead.");
#else
LoadSingletonPrefabAsync(AutoLoad.AddressableKey).WaitForCompletion();
singletonPrefab = LoadHandleIsValid() && m_LoadHandle.Result.TryGetComponent<T>(out var tObject) ? tObject : null;
#endif
}
#endif
InstantiateOrCreateSingleton(singletonPrefab ? singletonPrefab.gameObject : null);
}
return m_Instance;
}
}
protected static T m_Instance;
#endregion
#region UNITY MESSAGES
protected virtual void Awake() {
if (!EnsureSingletonInstance())
return;
if (IsAutoLoad && AutoLoad.IsPersistent)
DontDestroyOnLoad(this);
SingletonAwake();
}
protected virtual void Start() {
if (EnsureSingletonInstance())
SingletonStart();
}
protected virtual void OnEnable() {
if (EnsureSingletonInstance())
SingletonEnabled();
}
protected virtual void OnDisable() {
if (EnsureSingletonInstance())
SingletonDisabled();
}
protected virtual void OnDestroy() {
if (m_Instance == this) {
#if UNITY_ADDRESSABLES
if (LoadHandleIsValid())
Addressables.Release(m_LoadHandle);
m_LoadHandle = default;
m_LoadTask = null;
#endif
SingletonDestroy();
m_Instance = null;
}
}
/// <summary>Called once when singleton instance is created.</summary>
protected virtual void SingletonAwake() { }
/// <summary>Called on Start if this instance is the active singleton.</summary>
protected virtual void SingletonStart() { }
/// <summary>Called when singleton is enabled.</summary>
protected virtual void SingletonEnabled() { }
/// <summary>Called when singleton is disabled.</summary>
protected virtual void SingletonDisabled() { }
/// <summary>Called when singleton is destroyed.</summary>
protected virtual void SingletonDestroy() { }
#endregion
#region HELPERS
/// <summary>Instantiates a prefab or creates a new GameObject if prefab is null.</summary>
protected static void InstantiateOrCreateSingleton(GameObject prefab) {
if (prefab != null) {
var instance = Instantiate(prefab);
if (instance.TryGetComponent(out m_Instance))
m_Instance.name = $"[{Type.Name}]";
else {
Destroy(instance);
Debug.LogWarning($"Prefab '{prefab.name}' does not contain component '{Type.Name}'.");
}
}
// Fallback
if (m_Instance == null)
m_Instance = new GameObject($"[{Type.Name}]").AddComponent<T>();
}
///<summary> Ensures that this component is the valid singleton instance. Destroys duplicates if necessary.</summary>
protected virtual bool EnsureSingletonInstance() {
if (Instance == this)
return true;
if (Application.isPlaying) {
Debug.LogWarning($"Duplicate singleton '{Type.Name}' detected. Destroying '{name}'.");
Destroy(gameObject);
}
return false;
}
#endregion
#region ADDRESSABLES
#if UNITY_ADDRESSABLES
protected static AsyncOperationHandle<GameObject> m_LoadHandle;
protected static Task<T> m_LoadTask;
/// <summary>Async singleton retrieval via Addressables.</summary>
public static async Task<T> GetInstanceAsync(string key = "") {
if (!Application.isPlaying)
return null;
m_LoadTask ??= GetLoadTask(key);
return await m_LoadTask;
}
protected static async Task<T> GetLoadTask(string key = "") {
if (m_Instance != null)
return m_Instance;
m_Instance = FindFirstObjectByType<T>();
var addressableKey = !string.IsNullOrEmpty(key) ? key : AutoLoad?.AddressableKey;
if (m_Instance == null && !string.IsNullOrEmpty(addressableKey)) {
await LoadSingletonPrefabAsync(addressableKey).Task;
InstantiateOrCreateSingleton(LoadHandleIsValid() ? m_LoadHandle.Result : null);
}
return m_Instance;
}
protected static bool LoadHandleIsValid()
=> m_LoadHandle.IsValid() && m_LoadHandle.Status == AsyncOperationStatus.Succeeded;
protected static AsyncOperationHandle<GameObject> LoadSingletonPrefabAsync(string key) {
try {
m_LoadHandle = Addressables.LoadAssetAsync<GameObject>(key);
return m_LoadHandle;
}
catch { return default; }
}
#endif
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment