Last active
January 24, 2026 21:52
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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