128 lines
4.5 KiB
C#
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
|
||
|
}
|
||
|
}
|