using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; [Serializable] public class MemoryProgressChangedEvent : UnityEvent { } [Serializable] public class MemoryPieceAddedEvent : UnityEvent { } [DisallowMultipleComponent] public class MemoryProgressManager : MonoBehaviour { [Header("References")] [Tooltip("직접 연결 방식입니다. 자동 등록을 쓸 경우 비워둬도 됩니다.")] [SerializeField] private MemoryProgressUI[] memoryProgressUIs; [Header("Progress")] [SerializeField] private int requiredPieces = 5; [SerializeField] private int currentPieces = 0; [Header("Save / Load")] [Tooltip("체크하면 PlayerPrefs로 현재 기억의 조각 진행도를 저장합니다.")] [SerializeField] private bool usePlayerPrefsSave = true; [SerializeField] private string saveKey = "MemoryProgress_CurrentPieces"; [Tooltip("Awake에서 저장된 진행도를 불러옵니다.")] [SerializeField] private bool loadSavedProgressOnAwake = true; [Tooltip("진행도가 바뀔 때마다 자동 저장합니다.")] [SerializeField] private bool saveWheneverChanged = true; [Tooltip("ResetProgress() 호출 시 저장값도 0으로 갱신합니다. 테스트용 리셋이면 켜두는 것을 추천합니다.")] [SerializeField] private bool saveResetValue = true; [Header("Start Behaviour")] [Tooltip("Start에서 onProgressChanged를 한 번 호출합니다. 시작 시 다른 시스템도 현재 진행도를 받아야 하면 켜세요.")] [SerializeField] private bool notifyProgressChangedOnStart = false; [Tooltip("저장된 값이 이미 완료 상태일 때 Start에서 onAllPiecesCollected를 다시 호출합니다. 저장된 완료 상태로 문/스토리 이벤트를 복구해야 할 때 켜세요.")] [SerializeField] private bool invokeCompletionOnStartIfAlreadyCompleted = false; [Header("Events")] [Tooltip("진행도 값이 바뀔 때 호출됩니다. UI 갱신용으로 쓰세요. Reset / Set에서도 호출될 수 있습니다.")] public MemoryProgressChangedEvent onProgressChanged = new MemoryProgressChangedEvent(); [Tooltip("AddMemoryPiece로 실제 조각이 추가되었을 때만 호출됩니다. 획득 팝업은 이 이벤트에 연결하세요. 인자: addedAmount, current, required")] public MemoryPieceAddedEvent onMemoryPieceAdded = new MemoryPieceAddedEvent(); [Tooltip("모든 기억의 조각을 모았을 때 한 번만 호출됩니다.")] public UnityEvent onAllPiecesCollected = new UnityEvent(); [Header("Debug")] [SerializeField] private bool showDebugLog = true; private readonly List registeredUIs = new List(); private bool completionEventInvoked; public int CurrentPieces => currentPieces; public int RequiredPieces => requiredPieces; public bool IsCompleted => currentPieces >= requiredPieces; private void Awake() { ValidateProgressValues(); if (usePlayerPrefsSave && loadSavedProgressOnAwake) LoadProgress(); ValidateProgressValues(); // 저장된 값이 이미 완료 상태일 때 완료 이벤트가 중복 호출되지 않도록 기본적으로 막습니다. // 단, invokeCompletionOnStartIfAlreadyCompleted를 켜면 Start에서 한 번 호출합니다. completionEventInvoked = IsCompleted && !invokeCompletionOnStartIfAlreadyCompleted; } private void Start() { RefreshUI(); if (notifyProgressChangedOnStart) onProgressChanged?.Invoke(currentPieces, requiredPieces); if (invokeCompletionOnStartIfAlreadyCompleted && IsCompleted) CheckCompletion(); } public int AddMemoryPiece() { return AddMemoryPiece(1); } /// /// 기억의 조각을 추가합니다. /// 반환값은 실제로 추가된 개수입니다. /// 이미 완료 상태이거나 amount가 0 이하이면 0을 반환합니다. /// public int AddMemoryPiece(int amount) { if (amount <= 0) { if (showDebugLog) Debug.LogWarning("[MemoryProgressManager] 추가할 기억의 조각 수가 0 이하입니다.", this); return 0; } if (IsCompleted) { if (showDebugLog) Debug.Log("[MemoryProgressManager] 이미 모든 기억의 조각을 모았습니다.", this); return 0; } int before = currentPieces; currentPieces = Mathf.Clamp(currentPieces + amount, 0, requiredPieces); int addedAmount = currentPieces - before; if (addedAmount <= 0) return 0; RefreshAndNotify(); onMemoryPieceAdded?.Invoke(addedAmount, currentPieces, requiredPieces); SaveProgressIfNeeded(); CheckCompletion(); return addedAmount; } public void SetMemoryPieces(int value) { currentPieces = Mathf.Clamp(value, 0, requiredPieces); if (!IsCompleted) completionEventInvoked = false; RefreshAndNotify(); SaveProgressIfNeeded(); CheckCompletion(); } public void SetRequiredPieces(int value) { SetRequiredPieces(value, true); } public void SetRequiredPieces(int value, bool keepCurrentProgress) { requiredPieces = Mathf.Max(1, value); if (!keepCurrentProgress) currentPieces = 0; currentPieces = Mathf.Clamp(currentPieces, 0, requiredPieces); if (!IsCompleted) completionEventInvoked = false; RefreshAndNotify(); SaveProgressIfNeeded(); CheckCompletion(); } public void ResetProgress() { currentPieces = 0; completionEventInvoked = false; RefreshAndNotify(); if (usePlayerPrefsSave && saveResetValue) SaveProgress(); } public void ClearSavedProgress() { if (string.IsNullOrWhiteSpace(saveKey)) return; PlayerPrefs.DeleteKey(saveKey); PlayerPrefs.Save(); if (showDebugLog) Debug.Log($"[MemoryProgressManager] 저장된 진행도 삭제: {saveKey}", this); } public void SaveProgress() { if (!usePlayerPrefsSave) return; if (string.IsNullOrWhiteSpace(saveKey)) { Debug.LogWarning("[MemoryProgressManager] Save Key가 비어 있어 저장하지 않았습니다.", this); return; } PlayerPrefs.SetInt(saveKey, currentPieces); PlayerPrefs.Save(); if (showDebugLog) Debug.Log($"[MemoryProgressManager] 진행도 저장: {currentPieces}/{requiredPieces}", this); } public void LoadProgress() { if (!usePlayerPrefsSave) return; if (string.IsNullOrWhiteSpace(saveKey)) { Debug.LogWarning("[MemoryProgressManager] Save Key가 비어 있어 불러오지 않았습니다.", this); return; } currentPieces = PlayerPrefs.GetInt(saveKey, currentPieces); currentPieces = Mathf.Clamp(currentPieces, 0, requiredPieces); if (showDebugLog) Debug.Log($"[MemoryProgressManager] 진행도 불러오기: {currentPieces}/{requiredPieces}", this); } public void RegisterUI(MemoryProgressUI ui) { if (ui == null) return; if (!registeredUIs.Contains(ui)) registeredUIs.Add(ui); ui.SetProgress(currentPieces, requiredPieces); } public void UnregisterUI(MemoryProgressUI ui) { if (ui == null) return; registeredUIs.Remove(ui); } public void RefreshAllUIs() { RefreshUI(); } private void RefreshAndNotify() { RefreshUI(); onProgressChanged?.Invoke(currentPieces, requiredPieces); if (showDebugLog) Debug.Log($"[MemoryProgressManager] 기억의 조각 {currentPieces}/{requiredPieces}", this); } private void RefreshUI() { if (memoryProgressUIs != null) { for (int i = 0; i < memoryProgressUIs.Length; i++) { if (memoryProgressUIs[i] != null) memoryProgressUIs[i].SetProgress(currentPieces, requiredPieces); } } for (int i = registeredUIs.Count - 1; i >= 0; i--) { MemoryProgressUI ui = registeredUIs[i]; if (ui == null) { registeredUIs.RemoveAt(i); continue; } // 같은 UI가 직접 배열에도 들어 있고 자동 등록도 된 경우, 중복 갱신을 피합니다. if (IsInSerializedUIArray(ui)) continue; ui.SetProgress(currentPieces, requiredPieces); } } private bool IsInSerializedUIArray(MemoryProgressUI ui) { if (memoryProgressUIs == null || ui == null) return false; for (int i = 0; i < memoryProgressUIs.Length; i++) { if (memoryProgressUIs[i] == ui) return true; } return false; } private void CheckCompletion() { if (!IsCompleted) return; if (completionEventInvoked) return; completionEventInvoked = true; onAllPiecesCollected?.Invoke(); if (showDebugLog) Debug.Log("[MemoryProgressManager] 기억의 조각을 모두 모았습니다.", this); } private void SaveProgressIfNeeded() { if (usePlayerPrefsSave && saveWheneverChanged) SaveProgress(); } private void ValidateProgressValues() { requiredPieces = Mathf.Max(1, requiredPieces); currentPieces = Mathf.Clamp(currentPieces, 0, requiredPieces); } // ------------------------------------------------------------ // Test / Debug Helpers // Unity Button OnClick에서는 반환값이 int인 AddMemoryPiece(int)가 잘 안 보일 수 있습니다. // 그래서 버튼에서 바로 연결할 수 있는 void 테스트 메서드를 제공합니다. // 빌드에 포함되어도 동작은 하지만, 실제 배포용 버튼에는 연결하지 않는 것을 추천합니다. // ------------------------------------------------------------ [Header("Test / Debug")] [Tooltip("테스트용 메서드를 인스펙터 버튼 OnClick에 연결해서 진행도 UI를 빠르게 확인할 수 있습니다.")] [SerializeField] private bool enableTestMethods = true; public void TestAddOnePiece() { if (!enableTestMethods) { if (showDebugLog) Debug.Log("[MemoryProgressManager] 테스트 메서드가 비활성화되어 있습니다.", this); return; } AddMemoryPiece(1); } public void TestAddTwoPieces() { if (!enableTestMethods) { if (showDebugLog) Debug.Log("[MemoryProgressManager] 테스트 메서드가 비활성화되어 있습니다.", this); return; } AddMemoryPiece(2); } public void TestResetProgress() { if (!enableTestMethods) { if (showDebugLog) Debug.Log("[MemoryProgressManager] 테스트 메서드가 비활성화되어 있습니다.", this); return; } ResetProgress(); } public void TestSetProgressZero() { if (!enableTestMethods) { if (showDebugLog) Debug.Log("[MemoryProgressManager] 테스트 메서드가 비활성화되어 있습니다.", this); return; } SetMemoryPieces(0); } public void TestSetProgressOne() { if (!enableTestMethods) { if (showDebugLog) Debug.Log("[MemoryProgressManager] 테스트 메서드가 비활성화되어 있습니다.", this); return; } SetMemoryPieces(1); } public void TestSetProgressFour() { if (!enableTestMethods) { if (showDebugLog) Debug.Log("[MemoryProgressManager] 테스트 메서드가 비활성화되어 있습니다.", this); return; } SetMemoryPieces(Mathf.Max(0, requiredPieces - 1)); } public void TestCompleteProgress() { if (!enableTestMethods) { if (showDebugLog) Debug.Log("[MemoryProgressManager] 테스트 메서드가 비활성화되어 있습니다.", this); return; } SetMemoryPieces(requiredPieces); } public void TestClearSaveAndReset() { if (!enableTestMethods) { if (showDebugLog) Debug.Log("[MemoryProgressManager] 테스트 메서드가 비활성화되어 있습니다.", this); return; } ClearSavedProgress(); ResetProgress(); } [ContextMenu("TEST/Add One Piece")] private void ContextTestAddOnePiece() { TestAddOnePiece(); } [ContextMenu("TEST/Reset Progress")] private void ContextTestResetProgress() { TestResetProgress(); } [ContextMenu("TEST/Complete Progress")] private void ContextTestCompleteProgress() { TestCompleteProgress(); } [ContextMenu("TEST/Clear Save And Reset")] private void ContextTestClearSaveAndReset() { TestClearSaveAndReset(); } #if UNITY_EDITOR private void OnValidate() { ValidateProgressValues(); if (usePlayerPrefsSave && string.IsNullOrWhiteSpace(saveKey)) { Debug.LogWarning("MemoryProgressManager: Save Key가 비어 있습니다. 저장 기능을 쓰려면 고유한 키를 입력하세요.", this); } if (memoryProgressUIs != null) { for (int i = 0; i < memoryProgressUIs.Length; i++) { for (int j = i + 1; j < memoryProgressUIs.Length; j++) { if (memoryProgressUIs[i] != null && memoryProgressUIs[i] == memoryProgressUIs[j]) { Debug.LogWarning("MemoryProgressManager: Memory Progress UIs 배열에 같은 UI가 중복 등록되어 있습니다.", this); return; } } } } } #endif }