484 lines
14 KiB
C#
484 lines
14 KiB
C#
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("직접 연결 방식입니다. 자동 등록을 쓸 경우 비워둬도 됩니다.")]
|
|
[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<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()
|
|
{
|
|
RefreshUI();
|
|
|
|
if (notifyProgressChangedOnStart)
|
|
onProgressChanged?.Invoke(currentPieces, requiredPieces);
|
|
|
|
if (invokeCompletionOnStartIfAlreadyCompleted && IsCompleted)
|
|
CheckCompletion();
|
|
}
|
|
|
|
public int AddMemoryPiece()
|
|
{
|
|
return AddMemoryPiece(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 기억의 조각을 추가합니다.
|
|
/// 반환값은 실제로 추가된 개수입니다.
|
|
/// 이미 완료 상태이거나 amount가 0 이하이면 0을 반환합니다.
|
|
/// </summary>
|
|
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
|
|
}
|