2026.06.24

This commit is contained in:
2026-06-24 17:13:40 +09:00
parent b1e85a5b89
commit d601161390
86 changed files with 10287 additions and 336 deletions

View File

@@ -1,63 +1,128 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
[Serializable]
public class MemoryProgressChangedEvent : UnityEvent<int, int> { }
[Serializable]
public class MemoryPieceAddedEvent : UnityEvent<int, int, int> { }
[DisallowMultipleComponent]
public class MemoryProgressManager : MonoBehaviour
{
[Header("References")]
[Tooltip("진행도 UI가 여러 곳에 있으면 모두 넣으세요. 예: HUD용, 방 선택용")]
[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")]
public MemoryProgressChangedEvent onProgressChanged;
public UnityEvent onAllPiecesCollected;
[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<MemoryProgressUI> registeredUIs = new List<MemoryProgressUI>();
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()
{
requiredPieces = Mathf.Max(1, requiredPieces);
currentPieces = Mathf.Clamp(currentPieces, 0, requiredPieces);
RefreshUI();
if (notifyProgressChangedOnStart)
onProgressChanged?.Invoke(currentPieces, requiredPieces);
if (invokeCompletionOnStartIfAlreadyCompleted && IsCompleted)
CheckCompletion();
}
public void AddMemoryPiece()
public int AddMemoryPiece()
{
AddMemoryPiece(1);
return AddMemoryPiece(1);
}
public void AddMemoryPiece(int amount)
/// <summary>
/// 기억의 조각을 추가합니다.
/// 반환값은 실제로 추가된 개수입니다.
/// 이미 완료 상태이거나 amount가 0 이하이면 0을 반환합니다.
/// </summary>
public int AddMemoryPiece(int amount)
{
if (amount <= 0)
return;
{
if (showDebugLog)
Debug.LogWarning("[MemoryProgressManager] 추가할 기억의 조각 수가 0 이하입니다.", this);
return 0;
}
if (IsCompleted)
{
if (showDebugLog)
Debug.Log("[MemoryProgressManager] 이미 모든 기억의 조각을 모았습니다.");
Debug.Log("[MemoryProgressManager] 이미 모든 기억의 조각을 모았습니다.", this);
return;
return 0;
}
currentPieces += amount;
currentPieces = Mathf.Clamp(currentPieces, 0, requiredPieces);
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)
@@ -68,6 +133,29 @@ public void SetMemoryPieces(int value)
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();
}
@@ -76,6 +164,57 @@ 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)
@@ -83,28 +222,75 @@ 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}");
Debug.Log($"[MemoryProgressManager] 기억의 조각 {currentPieces}/{requiredPieces}", this);
}
private void RefreshUI()
{
if (memoryProgressUIs == null)
return;
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] != null)
memoryProgressUIs[i].SetProgress(currentPieces, requiredPieces);
if (memoryProgressUIs[i] == ui)
return true;
}
return false;
}
private void CheckCompletion()
@@ -119,6 +305,179 @@ private void CheckCompletion()
onAllPiecesCollected?.Invoke();
if (showDebugLog)
Debug.Log("[MemoryProgressManager] 기억의 조각을 모두 모았습니다.");
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
}