Merge remote-tracking branch 'origin/develop' into develop

# Conflicts:
#	Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset
This commit is contained in:
hho210 2025-08-19 19:22:08 +09:00
commit 574ca3e9d3
67 changed files with 412 additions and 222 deletions

7
.aiignore Normal file
View File

@ -0,0 +1,7 @@
# An .aiignore file follows the same syntax as a .gitignore file.
# .gitignore documentation: https://git-scm.com/docs/gitignore
# Junie will ask for explicit approval before view or edit the file or file within a directory listed in .aiignore.
# Only files contents is protected, Junie is still allowed to view file names even if they are listed in .aiignore.
# Be aware that the files you included in .aiignore can still be accessed by Junie in two cases:
# - If Brave Mode is turned on.
# - If a command has been added to the Allowlist — Junie will not ask for confirmation, even if it accesses - files and folders listed in .aiignore.

51
.junie/guidelines.md Normal file
View File

@ -0,0 +1,51 @@
# 프로젝트 가이드라인 — ProjectDDD (Unity)
이 저장소는 주로 Unity 에디터를 통해 편집 및 실행됩니다. 코드는 Unity의 .NET/Mono 환경을 대상으로 하며, 컴파일은 일반적인 `dotnet` CLI 빌드가 아니라 대개 Unity가 주도합니다.
## 저장소 레이아웃(상위 수준)
- `Assets/` 게임 콘텐츠와 스크립트. 대부분의 수정은 여기서 이루어집니다.
- `Packages/` Unity 패키지 매니페스트와 임베디드 패키지.
- `ProjectSettings/``UserSettings/` Unity 설정. 지시가 없는 한 수동으로 수정하지 마십시오.
- `Library/`, `Temp/`, `obj/`, `Logs/` Unity/IDE가 생성. 이 디렉터리는 수정하거나 커밋하지 마십시오.
- `ServerData/`, `Docs/` 프로젝트별 데이터와 문서가 있다면 여기에 있습니다.
- 다수의 `*.csproj` 파일 Rider/IDE 지원을 위한 자동 생성 파일; 특별히 필요하지 않는 한 수동 수정 금지.
## 이 프로젝트에서 Junie의 작업 방식
- 문제를 직접 해결하는 최소하고도 목표 지향적인 코드 변경을 선호합니다.
- Unity가 생성한 폴더들(`Library/`, `Temp/`, `obj/`, `Logs/`)이나 패키지 캐시 내용은 수정하지 않습니다.
- 새로운 스크립트는 `Assets/` 아래 적절한 도메인 폴더에 배치합니다. `_DDD/_Scripts` 하위의 기존 폴더 규칙을 유지합니다.
- `.csproj` 파일은 수정하지 않습니다. Unity가 재생성합니다.
- Addressables를 다룰 때는 그룹 설정을 일관되게 유지하고 의도치 않은 대용량 데이터 변경을 커밋하지 않습니다.
## 빌드, 실행, 테스트
- 기본 실행 환경은 Unity Editor/Player입니다. 독립 실행형 단위 테스트가 없을 수 있습니다.
- 작업이 명시적으로 요구하지 않는 한 .NET 테스트 러너나 CLI 빌드를 시도하지 말고, 컴파일은 Unity에 맡기십시오.
- 코드를 추가할 때는 Unity에서 컴파일 가능한지 확인하십시오(런타임 코드에서 에디터 전용 API 직접 호출 금지, 네임스페이스 올바름, 사용할 수 없는 API 사용 금지).
- 본 문서와 같은 순수 문서/설정 작업에서는 빌드가 필요 없습니다.
## 코딩 및 스타일 가이드(C#/Unity)
- 표준 C# 컨벤션을 따르십시오: 공용 타입과 멤버는 PascalCase, 지역 변수와 매개변수는 camelCase.
- private 직렬화 필드는 `[SerializeField] private Type _fieldName;` 형태를 선호하고, 필요 시 프로퍼티로 노출하십시오.
- Unity API 호출은 메인 스레드에서 수행하십시오. `Update`/`FixedUpdate` 내부의 과도한 할당을 피하십시오.
- `async`/`await`는 신중하게 사용하십시오. 대부분의 엔진 API 호출에는 Unity 메인 스레드 동기화가 필요합니다.
- 거대한 모놀리식 구조보다 작은, 역할에 집중된 컴포넌트 구성을 선호합니다. 프로젝트의 DDD 경계를 준수하십시오.
- null 체크와 가드 절을 추가하고, 개발 빌드에서는 명확한 메시지와 함께 빠르게 실패하도록 하십시오.
## Addressables 및 리소스
- `Addressables.LoadAssetAsync<T>`로 로드하고, 사용 후 핸들을 해제하여 누수를 방지하십시오.
- 일반 게임 플레이 코드에서 Addressable 그룹을 프로그래밍적으로 수정하지 마십시오. 그룹/라벨 변경은 명시적으로 필요하지 않는 한 에디터에서 수행하십시오.
## 수정 금지(명시적 지시가 없는 한)
- `Library/`, `Temp/`, `obj/`, `Logs/`, 그리고 `Library/PackageCache/` 아래의 Unity 패키지 캐시.
- 루트의 자동 생성 `*.csproj` 파일.
- 작업과 무관한 대용량 바이너리 에셋.
## 자동화 작업의 완료 정의
- 이슈를 완전히 충족하는 최소 변경을 제공합니다.
- 각 응답에 계획, 진행 상황, 다음 단계를 포함한 `<UPDATE>`를 포함합니다.
- 코드가 변경되었다면, 에지 케이스를 고려하고 런타임 경로에서 에디터 전용 API 사용을 피하십시오.
- 제출 전에 의도치 않은 파일(특히 생성 디렉터리와 Addressable 그룹)이 수정되지 않았는지 확인하십시오.
## 깃
- 깃 커밋 시, 한국어로 커밋 메시지를 작성합니다.
- 깃 커밋 메시지는 간결하고 핵심 정보만 포함해야 합니다.

View File

@ -2,7 +2,8 @@
"name": "Febucci.TextAnimator.Demo.Runtime",
"references": [
"Unity.TextMeshPro",
"Febucci.TextAnimator.Runtime"
"Febucci.TextAnimator.Runtime",
"Febucci.Attributes.Runtime"
],
"optionalUnityReferences": [],
"includePlatforms": [],

View File

@ -2,7 +2,8 @@
"name": "Febucci.TextAnimator.Editor",
"rootNamespace": "",
"references": [
"GUID:1e113d3b5d77bc04eab508251483e8ff"
"GUID:1e113d3b5d77bc04eab508251483e8ff",
"GUID:448b0b55421917e4784a8f2f7449081f"
],
"includePlatforms": [
"Editor"

View File

@ -3,7 +3,8 @@
"rootNamespace": "",
"references": [
"GUID:1e113d3b5d77bc04eab508251483e8ff",
"GUID:6055be8ebefd69e48b49212b09b47b2f"
"GUID:6055be8ebefd69e48b49212b09b47b2f",
"GUID:448b0b55421917e4784a8f2f7449081f"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@ -812,6 +812,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
_enableBlockImage: 1
_uiActionsInputBinding: {fileID: 11400000, guid: 99d3d87bd43df65488e757c43a308f36, type: 2}
_messageLabel: {fileID: 3495127426411772216}
_messageLabelLocalizeStringEvent: {fileID: 7334955628972040157}
_cancelButton: {fileID: 3014273876221658359}

View File

@ -1091,6 +1091,10 @@ PrefabInstance:
propertyPath: m_Name
value: CookwareTabButton
objectReference: {fileID: 0}
- target: {fileID: 1035128454163554855, guid: b8766f1471289d74bbcdc2f5ad979e8b, type: 3}
propertyPath: m_Navigation.m_Mode
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2720195383270857804, guid: b8766f1471289d74bbcdc2f5ad979e8b, type: 3}
propertyPath: m_text
value: "\uC7A5\uBE44"
@ -4858,6 +4862,10 @@ PrefabInstance:
propertyPath: m_Name
value: MenuTabButton
objectReference: {fileID: 0}
- target: {fileID: 1035128454163554855, guid: b8766f1471289d74bbcdc2f5ad979e8b, type: 3}
propertyPath: m_Navigation.m_Mode
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2720195383270857804, guid: b8766f1471289d74bbcdc2f5ad979e8b, type: 3}
propertyPath: m_text
value: "\uBA54\uB274"
@ -6328,6 +6336,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
_enableBlockImage: 1
_uiActionsInputBinding: {fileID: 11400000, guid: 8073fcaf56fc7c34e996d0d47044f146, type: 2}
_checklistView: {fileID: 7075966153492927588}
_inventoryView: {fileID: 3570087040626823091}
_itemDetailView: {fileID: 7657801840785021781}
@ -8348,6 +8357,10 @@ PrefabInstance:
propertyPath: m_Interactable
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1035128454163554855, guid: b8766f1471289d74bbcdc2f5ad979e8b, type: 3}
propertyPath: m_Navigation.m_Mode
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2720195383270857804, guid: b8766f1471289d74bbcdc2f5ad979e8b, type: 3}
propertyPath: m_text
value: "\uC810\uC6D0"

Binary file not shown.

View File

@ -76,9 +76,9 @@ public static async Task<List<T>> LoadAssetsByLabel<T>(string label) where T : U
return new List<T>();
}
public static async Task<SceneInstance> LoadScene(AssetReference assetReference, LoadSceneMode mode = LoadSceneMode.Additive)
public static async Task<SceneInstance> LoadScene(AssetReference assetReference, LoadSceneMode mode = LoadSceneMode.Additive, bool activateOnLoad = true)
{
var handle = Addressables.LoadSceneAsync(assetReference, mode);
var handle = Addressables.LoadSceneAsync(assetReference, mode, activateOnLoad);
await handle.Task;
if (handle.Status == AsyncOperationStatus.Succeeded)

View File

@ -203,6 +203,13 @@ public static void CreateAtlas(string path, string destPath)
if (objects.Count == 0) return;
// Validate destination path extension
if (!destPath.EndsWith(ExtenstionConstants.SpriteAtlasExtenstionLower, System.StringComparison.OrdinalIgnoreCase))
{
Debug.LogWarning($"[SpriteAtlas] destPath must end with .spriteatlas : {destPath}");
return;
}
Utils.MakeFolderFromFilePath(destPath);
var atlas = new SpriteAtlasAsset();
@ -216,9 +223,17 @@ public static void CreateAtlas(string path, string destPath)
atlas.Add(objects.ToArray());
SpriteAtlasAsset.Save(atlas, destPath);
AssetDatabase.Refresh();
var sai = (SpriteAtlasImporter)AssetImporter.GetAtPath(destPath);
// Ensure importer is created/applied synchronously before accessing it
AssetDatabase.ImportAsset(destPath, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
var sai = AssetImporter.GetAtPath(destPath) as SpriteAtlasImporter;
if (sai == null)
{
Debug.LogWarning($"[SpriteAtlas] Importer not ready for '{destPath}'. Skipping settings this pass.");
return;
}
sai.packingSettings = new SpriteAtlasPackingSettings
{
enableRotation = false,
@ -235,8 +250,11 @@ public static void CreateAtlas(string path, string destPath)
generateMipMaps = false
};
// 저장 후 설정 반영을 위해 동기 임포트, 그리고 즉시 패킹 수행
// Persist settings and reimport synchronously to apply them
AssetDatabase.WriteImportSettingsIfDirty(destPath);
AssetDatabase.ImportAsset(destPath, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
// Finally, pack the atlas for the active build target
var packedAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(destPath);
if (packedAtlas != null)
{
@ -253,10 +271,22 @@ public static void CreateSingleAtlas(string path, string destPath)
AssetDatabase.DeleteAsset(destPath);
}
// Validate destination path extension
if (!destPath.EndsWith(ExtenstionConstants.SpriteAtlasExtenstionLower, System.StringComparison.OrdinalIgnoreCase))
{
Debug.LogWarning($"[SpriteAtlas] destPath must end with .spriteatlas : {destPath}");
return;
}
Utils.MakeFolderFromFilePath(destPath);
var atlas = new SpriteAtlasAsset();
var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(path);
if (sprite == null)
{
Debug.LogWarning($"[SpriteAtlas] Source sprite not found at '{path}'. Skipping atlas: '{destPath}'");
return;
}
atlas.Add(new Object[] { sprite });
var spriteAtlasComponents = new List<IPostProcessorSpriteAtlas>();
@ -267,9 +297,17 @@ public static void CreateSingleAtlas(string path, string destPath)
}
SpriteAtlasAsset.Save(atlas, destPath);
AssetDatabase.Refresh();
var sai = (SpriteAtlasImporter)AssetImporter.GetAtPath(destPath);
// Ensure importer is created/applied synchronously before accessing it
AssetDatabase.ImportAsset(destPath, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
var sai = AssetImporter.GetAtPath(destPath) as SpriteAtlasImporter;
if (sai == null)
{
Debug.LogWarning($"[SpriteAtlas] Importer not ready for '{destPath}'. Skipping settings this pass.");
return;
}
sai.packingSettings = new SpriteAtlasPackingSettings
{
enableRotation = false,
@ -286,8 +324,11 @@ public static void CreateSingleAtlas(string path, string destPath)
generateMipMaps = false
};
// 저장 후 설정 반영을 위해 동기 임포트, 그리고 즉시 패킹 수행
// Persist settings and reimport synchronously to apply them
AssetDatabase.WriteImportSettingsIfDirty(destPath);
AssetDatabase.ImportAsset(destPath, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
// Finally, pack the atlas for the active build target
var packedAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(destPath);
if (packedAtlas != null)
{

View File

@ -15,6 +15,17 @@ private void Awake()
_cinemachineCamera = GetComponent<CinemachineCamera>();
}
private void OnEnable()
{
CameraManager.Instance.RegisterCamera(this);
}
private void OnDisable()
{
var cameraManager = CameraManager.Instance;
cameraManager?.UnRegisterCamera(this);
}
public int GetPriority() => _cinemachineCamera.Priority;
public void SetPriority(int newPriority) => _cinemachineCamera.Priority = newPriority;
public void SetFollowTarget(Transform target) => _cinemachineCamera.Follow = target;

View File

@ -2,7 +2,6 @@
using System.Threading.Tasks;
using Sirenix.OdinInspector;
using Unity.Cinemachine;
using UnityEngine;
namespace DDD
{
@ -44,12 +43,7 @@ public Task Init()
public void PostInit()
{
var cameraGameObjects = FindObjectsByType<CameraGameObject>(FindObjectsInactive.Include, FindObjectsSortMode.None);
foreach (var cameraGameObject in cameraGameObjects)
{
RegisterCamera(cameraGameObject);
}
_initializationTask.SetResult(true);
}
public void RegisterCamera(CameraGameObject cameraGameObject)
@ -72,16 +66,9 @@ public void SwitchCamera(CameraType cameraType, CinemachineBlendDefinition.Style
}
}
public async Task<CameraGameObject> GetCameraGameObject(CameraType cameraType)
public CameraGameObject GetCameraGameObject(CameraType cameraType)
{
await _initializationTask.Task;
if (_cameraGameObjects.TryGetValue(cameraType, out var cameraGameObject))
{
return cameraGameObject;
}
return null;
return _cameraGameObjects.GetValueOrDefault(cameraType);
}
}
}

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

@ -69,6 +69,34 @@ public void PostInit()
}
public async Task PreloadAll()
{
var flowToSceneMapping = GameFlowManager.Instance.GameFlowSceneMappingSo.FlowToSceneMapping;
foreach (var flowAssetPair in flowToSceneMapping)
{
if (_loadedSceneDatas.ContainsKey(flowAssetPair.Key)) continue;
var runtimeKey = GetRuntimeKey(flowAssetPair.Value);
if (_assetKeyToSceneData.TryGetValue(runtimeKey, out var existing))
{
_loadedSceneDatas[flowAssetPair.Key] = existing;
continue;
}
var instance = await AssetManager.LoadScene(flowAssetPair.Value, activateOnLoad:false);
if (!instance.Scene.IsValid())
{
Debug.LogError($"[SceneManager] {flowAssetPair.Key}의 씬 로딩 실패");
continue;
}
var data = new SceneData(instance.Scene, instance);
_loadedSceneDatas[flowAssetPair.Key] = data;
_assetKeyToSceneData[runtimeKey] = data;
}
}
public async Task PreloadScene(GameFlowState gameFlowState)
{
if (_loadedSceneDatas.ContainsKey(gameFlowState)) return;
@ -93,41 +121,10 @@ public async Task PreloadScene(GameFlowState gameFlowState)
return;
}
DeactivateScene(loadedInstance.Scene);
var data = new SceneData(loadedInstance.Scene, loadedInstance);
_loadedSceneDatas[gameFlowState] = data;
_assetKeyToSceneData[runtimeKey] = data;
}
public async Task PreloadAll()
{
var flowToSceneMapping = GameFlowManager.Instance.GameFlowSceneMappingSo.FlowToSceneMapping;
foreach (var flowAssetPair in flowToSceneMapping)
{
if (_loadedSceneDatas.ContainsKey(flowAssetPair.Key)) continue;
var runtimeKey = GetRuntimeKey(flowAssetPair.Value);
if (_assetKeyToSceneData.TryGetValue(runtimeKey, out var existing))
{
_loadedSceneDatas[flowAssetPair.Key] = existing;
continue;
}
var instance = await AssetManager.LoadScene(flowAssetPair.Value);
if (!instance.Scene.IsValid())
{
Debug.LogError($"[SceneManager] {flowAssetPair.Key}의 씬 로딩 실패");
continue;
}
DeactivateScene(instance.Scene);
var data = new SceneData(instance.Scene, instance);
_loadedSceneDatas[flowAssetPair.Key] = data;
_assetKeyToSceneData[runtimeKey] = data;
}
}
public async Task ActivateScene(GameFlowState newFlowState)
{
@ -148,19 +145,10 @@ public async Task ActivateScene(GameFlowState newFlowState)
{
await handler.OnBeforeSceneActivate();
}
foreach (var root in sceneData.Scene.GetRootGameObjects())
if (sceneData.SceneInstance.HasValue)
{
root.SetActive(true);
}
if (sceneData.Scene.IsValid())
{
UnityEngine.SceneManagement.SceneManager.SetActiveScene(sceneData.Scene);
}
else
{
Debug.LogError($"[SceneManager] {newFlowState}의 Scene이 유효하지 않습니다.");
await sceneData.SceneInstance.Value.ActivateAsync();
}
foreach (var handler in _sceneTransitionHandlerSo.Handlers.Where(handler => handler != null))
@ -169,22 +157,6 @@ public async Task ActivateScene(GameFlowState newFlowState)
}
}
public void DeactivateScene(GameFlowState gameFlowState)
{
if (_loadedSceneDatas.TryGetValue(gameFlowState, out var sceneData))
{
DeactivateScene(sceneData.Scene);
}
}
private void DeactivateScene(Scene scene)
{
foreach (var rootObject in scene.GetRootGameObjects())
{
rootObject.SetActive(false);
}
}
public async Task UnloadScene(GameFlowState gameFlowState)
{
if (_loadedSceneDatas.TryGetValue(gameFlowState, out var sceneData))

View File

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

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.InputSystem;
@ -20,30 +21,23 @@ public static IEnumerable<T> GetFlags<T>(this T input) where T : Enum
public abstract class PopupUi<T> : BasePopupUi where T : Enum
{
protected BaseUiActionsInputBindingSo<T> _baseUiActionsInputBindingSo;
[SerializeField, Required] protected BaseUiActionsInputBinding<T> _uiActionsInputBinding;
protected readonly List<(InputAction action, Action<InputAction.CallbackContext> handler)> _registeredHandlers = new();
public override InputActionMaps InputActionMaps => _baseUiActionsInputBindingSo.InputActionMaps;
public override InputActionMaps InputActionMaps => _uiActionsInputBinding.InputActionMaps;
private bool _isTopPopup => UiManager.Instance.PopupUiState.IsTopPopup(this);
private const string InputBindingSo = "InputBindingSo";
protected override async void TryRegister()
protected override void TryRegister()
{
base.TryRegister();
UiManager.Instance?.PopupUiState?.RegisterPopupUI(this);
string addressableKey = $"{GetType().Name}_{typeof(T).Name}_{InputBindingSo}";
_baseUiActionsInputBindingSo = await AssetManager.LoadAsset<BaseUiActionsInputBindingSo<T>>(addressableKey);
Debug.Assert(_baseUiActionsInputBindingSo != null, $"{GetType().Name} class InputBindingSo not found: {addressableKey}");
UiManager.Instance.PopupUiState.RegisterPopupUI(this);
foreach (var actionEnum in _baseUiActionsInputBindingSo.BindingActions.GetFlags())
foreach (var actionEnum in _uiActionsInputBinding.BindingActions.GetFlags())
{
if (actionEnum.Equals(default(T))) continue;
var inputAction = InputManager.Instance.GetAction(_baseUiActionsInputBindingSo.InputActionMaps, actionEnum.ToString());
var inputAction = InputManager.Instance.GetAction(_uiActionsInputBinding.InputActionMaps, actionEnum.ToString());
if (inputAction == null) continue;
var startedHandler = new Action<InputAction.CallbackContext>(context =>
@ -96,7 +90,7 @@ public override void Open(OpenPopupUiEvent evt)
if (UiManager.Instance.PopupUiState.IsTopPopup(this))
{
InputManager.Instance.SwitchCurrentActionMap(_baseUiActionsInputBindingSo.InputActionMaps);
InputManager.Instance.SwitchCurrentActionMap(_uiActionsInputBinding.InputActionMaps);
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sirenix.OdinInspector;
using UnityEngine;

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

@ -0,0 +1,14 @@
using System;
using Sirenix.OdinInspector;
using UnityEngine;
namespace DDD
{
public class BaseUiActionsInputBinding<T> : ScriptableObject where T : Enum
{
public InputActionMaps InputActionMaps;
[EnumToggleButtons]
public T BindingActions;
}
}

View File

@ -1,17 +0,0 @@
using System;
using Sirenix.OdinInspector;
using UnityEngine;
namespace DDD
{
public class BaseUiActionsInputBindingSo<T> : ScriptableObject where T : Enum
{
public InputActionMaps InputActionMaps;
[EnumToggleButtons]
public T BindingActions;
[ReadOnly, LabelText("Addressable Key")]
public string AddressableKey => $"{typeof(T).Name}_InputBindingSo";
}
}

View File

@ -0,0 +1,7 @@
using UnityEngine;
namespace DDD
{
[CreateAssetMenu(fileName = "_UiActionsInputBinding", menuName = "Ui/RestaurantActions_InputBindingSo")]
public class RestaurantActionsInputBinding : BaseUiActionsInputBinding<RestaurantActions> { }
}

View File

@ -1,7 +0,0 @@
using UnityEngine;
namespace DDD
{
[CreateAssetMenu(fileName = "_RestaurantActions_InputBindingSo", menuName = "Ui/RestaurantActions_InputBindingSo")]
public class RestaurantActionsInputBindingSo : BaseUiActionsInputBindingSo<RestaurantActions> { }
}

View File

@ -0,0 +1,7 @@
using UnityEngine;
namespace DDD
{
[CreateAssetMenu(fileName = "_UiActionsInputBinding", menuName = "Ui/RestaurantUiActions_InputBindingSo")]
public class RestaurantUiActionsInputBinding : BaseUiActionsInputBinding<RestaurantUiActions> { }
}

View File

@ -1,7 +0,0 @@
using UnityEngine;
namespace DDD
{
[CreateAssetMenu(fileName = "_RestaurantUiActions_InputBindingSo", menuName = "Ui/RestaurantUiActions_InputBindingSo")]
public class RestaurantUiActionsInputBindingSo : BaseUiActionsInputBindingSo<RestaurantUiActions> { }
}

View File

@ -1,22 +1,20 @@
using System.Threading.Tasks;
using UnityEngine;
namespace DDD
{
public class RestaurantPlayerCharacter : RestaurantCharacter
{
protected override void Awake()
protected override async void Awake()
{
base.Awake();
_ = Initialize();
PlayerManager.Instance.RegisterPlayer(gameObject);
}
private async Task Initialize()
protected override void Start()
{
PlayerManager.Instance.RegisterPlayer(gameObject);
var cameraObject = await CameraManager.Instance.GetCameraGameObject(CameraType.RestaurantBaseCamera);
cameraObject?.SetFollowAndLookAtTarget(transform);
base.Start();
var cameraObject = CameraManager.Instance.GetCameraGameObject(CameraType.RestaurantBaseCamera);
cameraObject.SetFollowAndLookAtTarget(transform);
}
}
}

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;
return Task.CompletedTask;
_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

View File

@ -2,7 +2,8 @@
"name": "DrawingPackageToolsEditor",
"rootNamespace": "",
"references": [
"GUID:f4059aaf6c60a4a58a177a2609feb769"
"GUID:f4059aaf6c60a4a58a177a2609feb769",
"GUID:de4e6084e6d474788bb8c799d6b461eb"
],
"includePlatforms": [
"Editor"

View File

@ -7,7 +7,8 @@
"GUID:f4059aaf6c60a4a58a177a2609feb769",
"GUID:de4e6084e6d474788bb8c799d6b461eb",
"GUID:734d92eba21c94caba915361bd5ac177",
"GUID:e0cd26848372d4e5c891c569017e11f1"
"GUID:e0cd26848372d4e5c891c569017e11f1",
"GUID:db11b4b5d7520bc479416b48c98206cb"
],
"includePlatforms": [
"Editor"

View File

@ -2,7 +2,8 @@
"name": "AstarPackageToolsEditor",
"rootNamespace": "",
"references": [
"GUID:f4059aaf6c60a4a58a177a2609feb769"
"GUID:f4059aaf6c60a4a58a177a2609feb769",
"GUID:de4e6084e6d474788bb8c799d6b461eb"
],
"includePlatforms": [
"Editor"