so 싱글톤 추가

This commit is contained in:
NTG 2025-08-19 13:51:42 +09:00
parent b8eec075ab
commit 9d57bcb040
33 changed files with 185 additions and 72 deletions

View File

@ -10,22 +10,22 @@ public class GameController : Singleton<GameController>, IManager, IGameFlowHand
{
[SerializeField] private AssetReference _gameData;
public GameData GameData { get; private set; }
public GameState GameState { get; private set; }
public GameData GetGameData() => GameData.Instance;
public GameState GetGameState() => GameState.Instance;
private List<FlowController> _gameFlowControllers = new();
private static readonly List<Type> GameFlowControllerTypes = new();
public void PreInit()
{
LoadOrCreateRestaurantState();
CreateGameState();
RegisterFlowHandler();
}
public async Task Init()
{
await LoadData();
await GameData.LoadData();
await GetGameData().LoadData();
await InitializeAllFlowControllers();
}
@ -33,10 +33,9 @@ public void PostInit()
{
}
private void LoadOrCreateRestaurantState()
private void CreateGameState()
{
// TODO : Load states from saved files. if none, create them.
GameState = ScriptableObject.CreateInstance<GameState>();
GameState.CreateScriptSingleton();
}
private void RegisterFlowHandler()
@ -49,7 +48,7 @@ private async Task InitializeAllFlowControllers()
// Create controllers and initialize them
foreach (var gameFlowControllerType in GameFlowControllerTypes)
{
// create new controllers from restaurantFlowControllerType
// create new controllers from gameFlowControllerType
var newController = ScriptableObject.CreateInstance(gameFlowControllerType);
var newFlowController = newController as FlowController;
_gameFlowControllers.Add(newFlowController);
@ -64,14 +63,6 @@ private async Task InitializeAllFlowControllers()
private async Task LoadData()
{
var gameDataHandle = _gameData.LoadAssetAsync<GameData>();
await gameDataHandle.Task;
GameData = gameDataHandle.Result;
Debug.Assert(GameData != null, "GameData is null");
await Task.CompletedTask;
}

View File

@ -5,7 +5,7 @@
namespace DDD
{
[CreateAssetMenu(fileName = "GameData", menuName = "GameData/GameData")]
public class GameData : ScriptableObject
public class GameData : ScriptSingleton<GameData>
{
[SerializeField] private AssetReference _gameLocalizationData;

View File

@ -97,7 +97,7 @@ public string GetString(string key)
var entryRef = key;
var locale = LocalizationSettings.SelectedLocale;
VariablesGroupAsset variables = GameController.Instance.GameData.LocalizationData.SmartStringVariableGroup;
VariablesGroupAsset variables = GameData.Instance.LocalizationData.SmartStringVariableGroup;
if (variables != null)
{
_singleArgBuffer.Clear();

View File

@ -50,8 +50,8 @@ public void PreInit() { }
public async Task Init()
{
var gameLevelStateSo = GameController.Instance.GameState.LevelState;
var restaurantStateSo = RestaurantController.Instance.RestaurantState.ManagementState;
var gameLevelStateSo = GameState.Instance.LevelState;
var restaurantStateSo = RestaurantState.Instance.ManagementState;
// 예시: day 초기 세팅 (없으면 생성, 타입 다르면 교체)
Set(_smartStringKeys[smartStringKey.Day], gameLevelStateSo.Level);
@ -71,7 +71,7 @@ public void PostInit()
EventBus.Register<SmartVariablesDirtyEvent>(this);
}
private RestaurantManagementState GetRestaurantState() => RestaurantController.Instance.RestaurantState.ManagementState;
private RestaurantManagementState GetRestaurantState() => RestaurantState.Instance.ManagementState;
public void Invoke(SmartVariablesDirtyEvent evt)
{
@ -113,7 +113,7 @@ public void RefreshChecklistTargets()
public void RefreshDay()
{
var gameLevelStateSo = GameController.Instance.GameState.LevelState;
var gameLevelStateSo = GameState.Instance.LevelState;
Set(_smartStringKeys[smartStringKey.Day], gameLevelStateSo.Level);
}
@ -179,7 +179,7 @@ public void SetEnum<TEnum>(string key, TEnum value) where TEnum : struct
return null;
}
var smartStringVariableGroup = GameController.Instance.GameData.LocalizationData.SmartStringVariableGroup;
var smartStringVariableGroup = GameData.Instance.LocalizationData.SmartStringVariableGroup;
if (smartStringVariableGroup.TryGetValue(key, out var existing))
{

View File

@ -3,7 +3,7 @@
namespace DDD
{
public class GameState : ScriptableObject
public class GameState : ScriptSingleton<GameState>
{
[SerializeField] private AssetReference _gameLevelState;

View File

@ -29,7 +29,7 @@ public class ChecklistView : MonoBehaviour, IEventHandler<TodayMenuAddedEvent>,
public void Initalize()
{
restaurantManagementStateSo = RestaurantController.Instance.RestaurantState.ManagementState;
restaurantManagementStateSo = RestaurantState.Instance.ManagementState;
_checklistDatas = new List<ChecklistData>(3);
_checklistDatas = GetComponentsInChildren<ChecklistData>().ToList();

View File

@ -40,7 +40,7 @@ public void Setup(ItemSlotUi ui, ItemViewModel model)
public RuntimeAnimatorController GetAnimatorController()
{
return RestaurantController.Instance.RestaurantData.ManagementData.InventorySlotUiAnimatorController;
return RestaurantData.Instance.ManagementData.InventorySlotUiAnimatorController;
}
public void OnInventoryChanged(ItemSlotUi ui)

View File

@ -38,8 +38,8 @@ private void OnDisable()
public void Initialize()
{
restaurantManagementStateSo = RestaurantController.Instance.RestaurantState.ManagementState;
restaurantManagementDataSo = RestaurantController.Instance.RestaurantData.ManagementData;
restaurantManagementStateSo = RestaurantState.Instance.ManagementState;
restaurantManagementDataSo = RestaurantData.Instance.ManagementData;
Debug.Assert(restaurantManagementDataSo != null, "_todayMenuDataSo != null");
Clear();

View File

@ -49,7 +49,7 @@ private void OnDisable()
public void Initialize()
{
restaurantManagementDataSo = RestaurantController.Instance.RestaurantData.ManagementData;
restaurantManagementDataSo = RestaurantData.Instance.ManagementData;
}
public void Invoke(ItemSlotSelectedEvent evt)

View File

@ -60,7 +60,7 @@ private void UpdateHoldProgress()
private void ProcessCompleteBatchAction()
{
if (RestaurantController.Instance.RestaurantState.ManagementState.GetChecklistStates().Any(state => state == false))
if (RestaurantState.Instance.ManagementState.GetChecklistStates().Any(state => state == false))
{
ShowChecklistFailedPopup();
}

View File

@ -10,7 +10,7 @@ public void OnAdded(ItemSlotUi itemSlotUi)
if (inventorySlotUiStrategy.CanCrafting(itemSlotUi))
{
RestaurantController.Instance.RestaurantState.ManagementState.TryAddTodayMenu(itemSlotUi.Model);
RestaurantState.Instance.ManagementState.TryAddTodayMenu(itemSlotUi.Model);
}
else
{
@ -25,7 +25,7 @@ public void OnRemoved(ItemSlotUi itemSlotUi)
{
if (itemSlotUi.Strategy is InventorySlotUiStrategy) return;
RestaurantController.Instance.RestaurantState.ManagementState.TryRemoveTodayMenu(itemSlotUi.Model);
RestaurantState.Instance.ManagementState.TryRemoveTodayMenu(itemSlotUi.Model);
}
}
}

View File

@ -35,7 +35,7 @@ public void Setup(ItemSlotUi ui, ItemViewModel model)
}
string markSpriteKey = null;
if (RestaurantController.Instance.RestaurantState.ManagementState.IsCookwareMatched(ui.Model.Id))
if (RestaurantState.Instance.ManagementState.IsCookwareMatched(ui.Model.Id))
{
markSpriteKey = SpriteConstants.CheckYesSpriteKey;
}
@ -51,7 +51,7 @@ public void Setup(ItemSlotUi ui, ItemViewModel model)
public RuntimeAnimatorController GetAnimatorController()
{
return RestaurantController.Instance.RestaurantData.ManagementData.TodayMenuSlotUiAnimatorController;
return RestaurantData.Instance.ManagementData.TodayMenuSlotUiAnimatorController;
}
}
}

View File

@ -23,8 +23,8 @@ private void OnDestroy()
public void Initialize()
{
restaurantManagementStateSo = RestaurantController.Instance.RestaurantState.ManagementState;
restaurantManagementDataSo = RestaurantController.Instance.RestaurantData.ManagementData;
restaurantManagementStateSo = RestaurantState.Instance.ManagementState;
restaurantManagementDataSo = RestaurantData.Instance.ManagementData;
foreach (Transform child in _todayFoodContent)
{

View File

@ -10,7 +10,7 @@ public void OnAdded(ItemSlotUi itemSlotUi)
if (inventorySlotUiStrategy.CanCrafting(itemSlotUi))
{
RestaurantController.Instance.RestaurantState.ManagementState.TryAddTodayCookware(itemSlotUi.Model);
RestaurantState.Instance.ManagementState.TryAddTodayCookware(itemSlotUi.Model);
}
else
{
@ -25,7 +25,7 @@ public void OnRemoved(ItemSlotUi itemSlotUi)
{
if (itemSlotUi.Strategy is InventorySlotUiStrategy) return;
RestaurantController.Instance.RestaurantState.ManagementState.TryRemoveTodayCookware(itemSlotUi.Model);
RestaurantState.Instance.ManagementState.TryRemoveTodayCookware(itemSlotUi.Model);
}
}
}

View File

@ -18,7 +18,7 @@ public void Setup(ItemSlotUi ui, ItemViewModel model)
}
string markSpriteKey = null;
if (RestaurantController.Instance.RestaurantState.ManagementState.IsTodayMenuMatched(ui.Model.Id))
if (RestaurantState.Instance.ManagementState.IsTodayMenuMatched(ui.Model.Id))
{
markSpriteKey = SpriteConstants.CheckYesSpriteKey;
}
@ -34,7 +34,7 @@ public void Setup(ItemSlotUi ui, ItemViewModel model)
public RuntimeAnimatorController GetAnimatorController()
{
return RestaurantController.Instance.RestaurantData.ManagementData.TodayMenuSlotUiAnimatorController;
return RestaurantData.Instance.ManagementData.TodayMenuSlotUiAnimatorController;
}
}
}

View File

@ -23,8 +23,8 @@ private void OnDestroy()
public void Initialize()
{
restaurantManagementStateSo = RestaurantController.Instance.RestaurantState.ManagementState;
restaurantManagementDataSo = RestaurantController.Instance.RestaurantData.ManagementData;
restaurantManagementStateSo = RestaurantState.Instance.ManagementState;
restaurantManagementDataSo = RestaurantData.Instance.ManagementData;
foreach (Transform child in _todayWorkerContent)
{

View File

@ -26,7 +26,7 @@ public void Setup(ItemSlotUi ui, ItemViewModel model)
public RuntimeAnimatorController GetAnimatorController()
{
return RestaurantController.Instance.RestaurantData.ManagementData.TodayMenuSlotUiAnimatorController;
return RestaurantData.Instance.ManagementData.TodayMenuSlotUiAnimatorController;
}
}
}

View File

@ -9,7 +9,7 @@ public class RestaurantPlayerInput : MonoBehaviour
private void Start()
{
_playerDataSo = RestaurantController.Instance.RestaurantData.PlayerData;
_playerDataSo = RestaurantData.Instance.PlayerData;
_playerDataSo.OpenManagementUiAction = InputManager.Instance.GetAction(InputActionMaps.Restaurant, nameof(RestaurantActions.OpenManagementUi));
_playerDataSo.OpenManagementUiAction.performed += OnOpenManagementUi;

View File

@ -17,7 +17,7 @@ protected override void Start()
private Task Initialize()
{
_restaurantPlayerDataSo = RestaurantController.Instance.RestaurantData.PlayerData;
_restaurantPlayerDataSo = RestaurantData.Instance.PlayerData;
Debug.Assert(_restaurantPlayerDataSo != null, "_restaurantPlayerDataSo is null");
_restaurantPlayerDataSo!.InteractAction = InputManager.Instance.GetAction(InputActionMaps.Restaurant, nameof(RestaurantActions.Interact));

View File

@ -81,7 +81,7 @@ private System.Threading.Tasks.Task InitializePlayerData()
{
try
{
_playerDataSo = RestaurantController.Instance.RestaurantData.PlayerData;
_playerDataSo = RestaurantData.Instance.PlayerData;
SubscribeToInputEvents();
_isInitialized = true;
}

View File

@ -13,7 +13,7 @@ public override Task InitializeController()
public override Task InitializeState()
{
_environmentState = RestaurantController.Instance.RestaurantState.EnvironmentState;
_environmentState = RestaurantState.Instance.EnvironmentState;
return Task.CompletedTask;
}

View File

@ -13,7 +13,7 @@ public override Task InitializeController()
public override Task InitializeState()
{
// Load default asset
RestaurantController.Instance.RestaurantState.ManagementState.InitializeReadyForRestaurant();
RestaurantState.Instance.ManagementState.InitializeReadyForRestaurant();
return Task.CompletedTask;
}

View File

@ -8,14 +8,13 @@ public class RestaurantRunController : FlowController
RestaurantCustomerState _restaurantCustomerStateSo;
public override Task InitializeController()
{
_restaurantCustomerStateSo = RestaurantController.Instance.RestaurantState.CustomerState;
_restaurantCustomerStateSo = RestaurantState.Instance.CustomerState;
return Task.CompletedTask;
}
public override Task InitializeState()
{
return Task.CompletedTask;
}
public override async Task OnReadyNewFlow(GameFlowState newFlowState)

View File

@ -15,7 +15,7 @@ public override Task RunFlowTask()
{
// TODO : Base prefab from EnvironmentDataSo
var props = RestaurantController.Instance.RestaurantState.EnvironmentState.Props;
var props = RestaurantState.Instance.EnvironmentState.Props;
foreach (var prop in props)
{
// TODO : Instantiate and Initialize

View File

@ -20,7 +20,7 @@ public override Task RunFlowTask()
return Task.CompletedTask;
}
var playerPrefab = RestaurantController.Instance.RestaurantData.PlayerData.PlayerPrefab;
var playerPrefab = RestaurantData.Instance.PlayerData.PlayerPrefab;
if (playerPrefab == null)
{
Debug.LogError("PlayerPrefab이 설정되지 않았습니다!");

View File

@ -10,8 +10,8 @@ public class RestaurantController : Singleton<RestaurantController>, IManager, I
{
[SerializeField] private AssetReference _restaurantData;
public RestaurantData RestaurantData { get; private set; }
public RestaurantState RestaurantState { get; private set; }
public RestaurantData GetRestaurantData() => RestaurantData.Instance;
public RestaurantState GetRestaurantState() => RestaurantState.Instance;
private List<FlowController> _restaurantFlowControllers = new();
@ -27,14 +27,14 @@ public class RestaurantController : Singleton<RestaurantController>, IManager, I
public void PreInit()
{
LoadOrCreateRestaurantState();
CreateRestaurantState();
RegisterFlowHandler();
}
public async Task Init()
{
await LoadData();
await RestaurantData.LoadData();
await GetRestaurantData().LoadData();
await InitializeAllFlowControllers();
}
@ -42,10 +42,9 @@ public void PostInit()
{
}
private void LoadOrCreateRestaurantState()
private void CreateRestaurantState()
{
// TODO : Load states from saved files. if none, create them.
RestaurantState = ScriptableObject.CreateInstance<RestaurantState>();
RestaurantState.CreateScriptSingleton();
}
private void RegisterFlowHandler()
@ -73,13 +72,7 @@ private async Task InitializeAllFlowControllers()
private async Task LoadData()
{
var restaurantDataHandle = _restaurantData.LoadAssetAsync<RestaurantData>();
await restaurantDataHandle.Task;
RestaurantData = restaurantDataHandle.Result;
Debug.Assert(RestaurantData != null, "RestaurantData is null");
await Task.CompletedTask;
}
public async Task OnReadyNewFlow(GameFlowState newFlowState)

View File

@ -5,7 +5,7 @@
namespace DDD
{
[CreateAssetMenu(fileName = "RestaurantData", menuName = "RestaurantData/RestaurantData", order = 0)]
public class RestaurantData : ScriptableObject
public class RestaurantData : ScriptSingleton<RestaurantData>
{
[SerializeField] private AssetReference _restaurantPlayerData;
[SerializeField] private AssetReference _restaurantManagementData;

View File

@ -15,7 +15,7 @@ public bool ExecuteInteraction(IInteractor interactor, IInteractable interactabl
private RestaurantManagementState GetManagementState()
{
return RestaurantController.Instance.RestaurantState.ManagementState;
return RestaurantState.Instance.ManagementState;
}
public bool CanExecuteInteraction(IInteractor interactor = null, IInteractable interactable = null, ScriptableObject payloadSo = null)

View File

@ -51,7 +51,7 @@ private async Task InitializeRunRestaurant()
{
_iCustomerFactory = new CustomerFactory();
var currentGameLevel = GameController.Instance.GameState.LevelState.Level;
var currentGameLevel = GameState.Instance.LevelState.Level;
if (_levelDataSo == null)
{
_levelDataSo = DataManager.Instance.GetDataSo<LevelDataSo>();

View File

@ -86,7 +86,7 @@ public bool IsOpenable()
public RestaurantManagementData GetManagementData()
{
return RestaurantController.Instance.RestaurantData.ManagementData;
return RestaurantData.Instance.ManagementData;
}
public bool TryAddTodayMenu(ItemViewModel model)

View File

@ -2,7 +2,7 @@
namespace DDD
{
public class RestaurantState : ScriptableObject
public class RestaurantState : ScriptSingleton<RestaurantState>
{
public RestaurantManagementState ManagementState { get; private set; }
public RestaurantRunState RunState { get; private set; }

View File

@ -0,0 +1,128 @@
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
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0544b64d4ef0a744dbd9ee6bcf4ecc00