// 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() { }
}
}