ProjectDDD/Assets/_DDD/_Scripts/Utilities/ScriptSingleton.cs
2025-08-19 13:51:42 +09:00

128 lines
4.5 KiB
C#

using System;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace DDD
{
/// <summary>
/// Addressables를 통해 ScriptableObject 에셋을 로드하여 싱글톤으로 제공하는 베이스 클래스.
/// - 첫 접근 시 Addressables에서 타입명(네임스페이스 제외)을 키로 동기 로드합니다.
/// - 로드에 실패하면 예외를 발생합니다. (새로 생성하지 않습니다)
/// - 이미 로드된 경우 캐시된 인스턴스를 반환합니다.
/// </summary>
/// <typeparam name="T">구현 타입</typeparam>
public abstract class ScriptSingleton<T> : ScriptableObject where T : ScriptSingleton<T>
{
#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<T>(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
/// <summary>
/// 새로운 인스턴스를 생성하고 싱글톤으로 등록합니다.
/// Addressables에 등록되지 않은 경우 사용합니다.
/// </summary>
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<T>();
_instance = newInstance;
_instance.hideFlags = HideFlags.DontUnloadUnusedAsset;
_instance.OnInstanceLoaded();
Debug.Log($"[ScriptSingleton] {typeof(T).Name} 인스턴스를 생성하고 싱글톤으로 등록했습니다.");
return _instance;
}
}
/// <summary>
/// 사용자 정의 초기화 훅. 인스턴스가 로드된 뒤 1회 호출됩니다.
/// </summary>
protected virtual void OnInstanceLoaded() { }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
private static void RegisterQuitHandler()
{
Application.quitting -= OnApplicationQuitting;
Application.quitting += OnApplicationQuitting;
}
private static void OnApplicationQuitting()
{
_isQuitting = true;
}
/// <summary>
/// Address Key를 해석합니다. 요구사항에 따라 타입명(네임스페이스 제외) 그대로를 사용합니다.
/// </summary>
private static string ResolveAddressKey()
{
return typeof(T).Name;
}
#endregion
}
}