using System; using JetBrains.Annotations; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; namespace DDD { /// /// Addressables를 통해 ScriptableObject 에셋을 로드하여 싱글톤으로 제공하는 베이스 클래스. /// - 첫 접근 시 Addressables에서 타입명(네임스페이스 제외)을 키로 동기 로드합니다. /// - 로드에 실패하면 예외를 발생합니다. (새로 생성하지 않습니다) /// - 이미 로드된 경우 캐시된 인스턴스를 반환합니다. /// /// 구현 타입 public abstract class ScriptSingleton : ScriptableObject where T : ScriptSingleton { #region Fields [CanBeNull] private static T _instance; [NotNull] private static readonly object _lock = new(); private static bool _isQuitting; #endregion #region Properties [NotNull] public static T Instance { get { if (_instance != null) return _instance; if (_isQuitting) throw new InvalidOperationException($"애플리케이션 종료 중에는 '{typeof(T).Name}' 인스턴스를 로드할 수 없습니다."); lock (_lock) { // 이중 체크 락킹 패턴 if (_instance != null) return _instance; try { var key = ResolveAddressKey(); var handle = Addressables.LoadAssetAsync(key); // 동기 로드: 메인 스레드에서 호출할 것을 권장합니다. var loaded = handle.WaitForCompletion(); if (handle.Status != AsyncOperationStatus.Succeeded || loaded == null) { throw new InvalidOperationException($"Addressables 로드 실패: 타입='{typeof(T).Name}', key='{key}'"); } _instance = loaded; _instance.hideFlags = HideFlags.DontUnloadUnusedAsset; _instance.OnInstanceLoaded(); return _instance; } catch (Exception) { throw; } } } } #endregion #region Methods /// /// 새로운 인스턴스를 생성하고 싱글톤으로 등록합니다. /// Addressables에 등록되지 않은 경우 사용합니다. /// public static T CreateScriptSingleton() { if (_instance != null) { Debug.LogWarning($"[ScriptSingleton] {typeof(T).Name} 인스턴스가 이미 존재합니다. 기존 인스턴스를 반환합니다."); return _instance; } lock (_lock) { if (_instance != null) return _instance; var newInstance = ScriptableObject.CreateInstance(); _instance = newInstance; _instance.hideFlags = HideFlags.DontUnloadUnusedAsset; _instance.OnInstanceLoaded(); Debug.Log($"[ScriptSingleton] {typeof(T).Name} 인스턴스를 생성하고 싱글톤으로 등록했습니다."); return _instance; } } /// /// 사용자 정의 초기화 훅. 인스턴스가 로드된 뒤 1회 호출됩니다. /// protected virtual void OnInstanceLoaded() { } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] private static void RegisterQuitHandler() { Application.quitting -= OnApplicationQuitting; Application.quitting += OnApplicationQuitting; } private static void OnApplicationQuitting() { _isQuitting = true; } /// /// Address Key를 해석합니다. 요구사항에 따라 타입명(네임스페이스 제외) 그대로를 사용합니다. /// private static string ResolveAddressKey() { return typeof(T).Name; } #endregion } }