// MIT License - Copyright (c) 2024 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Utils { using System; using System.Runtime.CompilerServices; using System.Threading; using Core.Attributes; using Core.Extension; using Core.Helper; using UnityEngine; #if ODIN_INSPECTOR using Sirenix.OdinInspector; #endif /// /// Provides a simple, robust runtime singleton pattern for components. /// Ensures there is at most one active instance of . /// /// /// Access the global instance via ; if no active instance exists, /// a new named "<Type>-Singleton" is created and the component is added. /// /// Lifecycle: /// - On first access, searches for an active instance; otherwise creates one. /// - In , sets the static instance and, when is true and in play mode, /// detaches and calls to persist across scene loads. /// - In , detects duplicate instances and destroys the newer one. /// - Instance cache is cleared on domain reload before scene load. /// /// ODIN compatibility: When the ODIN_INSPECTOR symbol is defined, this class derives from /// Sirenix.OdinInspector.SerializedMonoBehaviour for richer serialization; otherwise it derives from /// . /// /// Concrete singleton component type that derives from this base. [DisallowMultipleComponent] public abstract class RuntimeSingleton : #if ODIN_INSPECTOR SerializedMonoBehaviour #else MonoBehaviour #endif where T : RuntimeSingleton { /// /// Gets a value indicating whether an instance is currently assigned. /// public static bool HasInstance => _instance != null; public static long InitializeCount => Interlocked.Read(ref _initializeCount); protected static long _initializeCount; protected internal static T _instance; static RuntimeSingleton() { RuntimeSingletonRegistry.Register(ClearInstance); } /// /// Gets a value that controls whether the instance persists across scene loads. /// Defaults to true. Override and return false to keep the instance /// scene‑local. /// protected virtual bool Preserve => true; protected virtual bool LogErrorOnDestruction => true; /// /// Gets the global instance, creating one if needed. /// /// /// /// public sealed class GameServices : RuntimeSingleton<GameServices> /// { /// protected override bool Preserve => false; // stay scene‑local /// public void Log(string msg) => Debug.Log(msg); /// } /// /// // Usage from anywhere /// GameServices.Instance.Log("Hello"); /// /// public static T Instance { get { if (_instance != null) { return _instance; } UnityMainThreadGuard.EnsureMainThread(); _instance = FindAnyObjectByType(FindObjectsInactive.Exclude); if (_instance != null) { return _instance; } Type type = typeof(T); GameObject instance = new($"{type.Name}-Singleton", type); if (_instance == null) { _ = instance.TryGetComponent(out _instance); } return _instance; } } internal static void ClearInstance() { _instance.Destroy(); Interlocked.Exchange(ref _initializeCount, 0); _instance = null; } protected virtual void Awake() { Interlocked.Increment(ref _initializeCount); this.AssignRelationalComponents(); if (_instance == null) { _instance = Unsafe.As(this); } if (Preserve && Application.isPlaying) { transform.SetParent(null, worldPositionStays: false); DontDestroyOnLoad(gameObject); } } protected virtual void Start() { if (_instance == null || _instance == this) { return; } string duplicateMessage = $"Double singleton detected, {_instance.name} conflicts with {name}. Total initialize count: {InitializeCount}."; if (LogErrorOnDestruction) { Debug.LogError(duplicateMessage); } else { Debug.Log(duplicateMessage); } gameObject.Destroy(); } protected virtual void OnDestroy() { if (_instance == this) { _instance = null; } } protected virtual void OnApplicationQuit() { } } }