using System.Collections; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [DisallowMultipleComponent] public class MazeUIManager : MonoBehaviour { public enum CompassUIState { None, Available, Used } [Header("HUD")] [SerializeField] private GameObject hudPanel; [SerializeField] private TextMeshProUGUI objectiveText; [SerializeField] private TextMeshProUGUI memoryPieceText; [SerializeField] private TextMeshProUGUI compassStateText; [Header("Compass UI")] [SerializeField] private Button compassButton; [SerializeField] private TextMeshProUGUI compassButtonText; [Header("Icon HUD / Socket Overlay")] [Tooltip("기억의 조각 홈 안에 들어가는 파란 수정 아이콘입니다. 홈 이미지는 연결하지 않아도 됩니다.")] [SerializeField] private Image memoryCrystalIcon; [Tooltip("나침반 버튼 안의 나침반 이미지입니다. 사용 가능/완료 색상만 바꿉니다.")] [SerializeField] private Image compassIcon; [Tooltip("나침반 사용 가능 상태를 알려주는 작은 느낌표 아이콘입니다.")] [SerializeField] private GameObject hintAlertIcon; [Tooltip("체크포인트 홈 위에 얹는 별 아이콘들입니다. 1번 체크포인트부터 순서대로 넣으세요.")] [SerializeField] private Image[] checkpointStarIcons; [Tooltip("체크하면 미도달 별을 끄고, 소켓 홈만 보이게 합니다.")] [SerializeField] private bool hideInactiveCheckpointStars = true; [Tooltip("체크하면 기억의 조각 미획득 시 수정 아이콘을 끄고, 빈 홈만 보이게 합니다.")] [SerializeField] private bool hideMemoryCrystalWhenMissing = true; [Tooltip("나침반 사용 가능 상태에서 느낌표 아이콘을 켭니다.")] [SerializeField] private bool showHintAlertWhenCompassAvailable = true; [Header("Guide Message UI")] [SerializeField] private GameObject guidePanel; [SerializeField] private CanvasGroup guideCanvasGroup; [SerializeField] private TextMeshProUGUI guideText; [SerializeField] private float guideShowTime = 2.5f; [SerializeField] private float fadeTime = 0.25f; [Header("Result UI")] [SerializeField] private GameObject resultPanel; [SerializeField] private TextMeshProUGUI resultTitleText; [SerializeField] private TextMeshProUGUI resultBodyText; [SerializeField] private Button retryButton; [SerializeField] private Button exitButton; [SerializeField] private TextMeshProUGUI retryButtonText; [SerializeField] private TextMeshProUGUI exitButtonText; [Header("Result Placement")] [Tooltip("연결하면 결과창을 켜는 순간 카메라 앞에 즉시 배치합니다.")] [SerializeField] private MazeMixedUIPanelFollower panelFollower; [SerializeField] private bool placeResultInFrontWhenShown = true; [Header("Result Button Text")] [SerializeField] private string retryButtonLabel = "다시하기"; [SerializeField] private string exitButtonLabel = "계속하기"; [Header("Result Reward UI")] [Tooltip("기억의 조각 뒤에 깔리는 푸른 빛 효과 오브젝트입니다. 없어도 됩니다.")] [SerializeField] private GameObject resultRewardGlow; [Tooltip("결과창 중앙 보상 홈 안에 들어가는 기억의 조각 아이콘입니다. 결과창용 소켓 이미지는 연결하지 않아도 됩니다.")] [SerializeField] private Image resultMemoryCrystalIcon; [Tooltip("결과창 체크포인트 홈 위에 얹는 별 아이콘들입니다. 1번부터 순서대로 넣으세요.")] [SerializeField] private Image[] resultCheckpointStarIcons; [Tooltip("체크하면 결과창의 미도달 별은 끄고, 결과창 체크포인트 홈만 보이게 합니다.")] [SerializeField] private bool hideInactiveResultCheckpointStars = true; [Tooltip("체크하면 결과창에서 기억의 조각을 획득하지 못한 상태일 때 수정 아이콘을 끕니다.")] [SerializeField] private bool hideResultMemoryCrystalWhenMissing = true; [Header("Current Maze Text")] [SerializeField] private string startObjectiveMessage = "목표: 나침반의 발자국 힌트를 따라 미로 출구를 찾아라"; [SerializeField] private string memoryMissingMessage = "기억의 조각: 미획득"; [SerializeField] private string memoryCollectedMessage = "기억의 조각: 획득"; [Header("Result Text")] [SerializeField] private string successResultTitleMessage = "기억의 조각을 되찾았다!"; [SerializeField] private string successResultBodyMessage = "별빛 발자국을 따라 미로를 통과했습니다."; [SerializeField] private string failResultTitleMessage = "미로 탐험 실패"; [SerializeField] private string failResultBodyMessage = "다시 도전해 보세요."; [Header("UI Colors")] [SerializeField] private Color normalColor = Color.white; [SerializeField] private Color successColor = Color.green; [SerializeField] private Color failColor = Color.red; [SerializeField] private Color disabledColor = Color.gray; [Header("Icon Colors")] [SerializeField] private Color iconActiveColor = Color.white; [SerializeField] private Color iconInactiveColor = new Color(1f, 1f, 1f, 0.25f); [Header("Result Colors")] [SerializeField] private Color resultSuccessTitleColor = new Color(1f, 0.78f, 0.36f, 1f); [SerializeField] private Color resultFailTitleColor = new Color(1f, 0.35f, 0.35f, 1f); [Header("Button Events")] public UnityEvent OnCompassButtonClicked = new UnityEvent(); public UnityEvent OnRetryButtonClicked = new UnityEvent(); public UnityEvent OnExitButtonClicked = new UnityEvent(); private Coroutine guideRoutine; private void Awake() { EnsureEvents(); if (compassButton != null) compassButton.onClick.AddListener(HandleCompassButtonClicked); if (retryButton != null) retryButton.onClick.AddListener(HandleRetryButtonClicked); if (exitButton != null) exitButton.onClick.AddListener(HandleExitButtonClicked); HideResult(); HideGuideImmediately(); } private void EnsureEvents() { if (OnCompassButtonClicked == null) OnCompassButtonClicked = new UnityEvent(); if (OnRetryButtonClicked == null) OnRetryButtonClicked = new UnityEvent(); if (OnExitButtonClicked == null) OnExitButtonClicked = new UnityEvent(); } private void OnDestroy() { if (compassButton != null) compassButton.onClick.RemoveListener(HandleCompassButtonClicked); if (retryButton != null) retryButton.onClick.RemoveListener(HandleRetryButtonClicked); if (exitButton != null) exitButton.onClick.RemoveListener(HandleExitButtonClicked); } public void HandleCompassButtonClicked() { OnCompassButtonClicked?.Invoke(); } public void HandleRetryButtonClicked() { OnRetryButtonClicked?.Invoke(); } public void HandleExitButtonClicked() { OnExitButtonClicked?.Invoke(); } public void ShowHUD() { if (hudPanel != null) hudPanel.SetActive(true); } public void HideHUD() { if (hudPanel != null) hudPanel.SetActive(false); } public void SetObjectiveSearchingMemory() { SetObjective(startObjectiveMessage); } public void SetObjectiveReturningToExit() { SetObjective("목표: 출구로 이동해 기억의 조각을 획득하라"); } public void SetStartObjective() { SetObjective(startObjectiveMessage); } public void SetObjective(string message) { if (objectiveText != null) objectiveText.text = message; } public void SetMemoryPieceState(bool hasMemoryPiece) { if (memoryPieceText != null) { if (hasMemoryPiece) { memoryPieceText.text = memoryCollectedMessage; memoryPieceText.color = successColor; } else { memoryPieceText.text = memoryMissingMessage; memoryPieceText.color = normalColor; } } if (memoryCrystalIcon != null) { if (hideMemoryCrystalWhenMissing) memoryCrystalIcon.gameObject.SetActive(hasMemoryPiece); else { memoryCrystalIcon.gameObject.SetActive(true); memoryCrystalIcon.color = hasMemoryPiece ? iconActiveColor : iconInactiveColor; } } } public void SetCheckpointProgress(int reachedCount, int totalCount) { SetStarProgress(checkpointStarIcons, reachedCount, totalCount, hideInactiveCheckpointStars); } public void SetHintAlertVisible(bool visible) { if (hintAlertIcon != null) hintAlertIcon.SetActive(visible); } public void SetCompassState(CompassUIState state) { switch (state) { case CompassUIState.None: SetCompassNone(); break; case CompassUIState.Available: SetCompassAvailable(); break; case CompassUIState.Used: SetCompassUsed(); break; } } public void SetCompassNone() { if (compassStateText != null) { compassStateText.text = "나침반: 없음"; compassStateText.color = disabledColor; } if (compassButtonText != null) compassButtonText.text = "나침반 없음"; if (compassButton != null) compassButton.interactable = false; if (compassIcon != null) compassIcon.color = iconInactiveColor; SetHintAlertVisible(false); } public void SetCompassAvailable() { if (compassStateText != null) { compassStateText.text = "나침반: 사용 가능"; compassStateText.color = normalColor; } if (compassButtonText != null) compassButtonText.text = "나침반 사용"; if (compassButton != null) compassButton.interactable = true; if (compassIcon != null) compassIcon.color = iconActiveColor; SetHintAlertVisible(showHintAlertWhenCompassAvailable); } public void SetCompassUsed() { if (compassStateText != null) { compassStateText.text = "나침반: 사용 완료"; compassStateText.color = disabledColor; } if (compassButtonText != null) compassButtonText.text = "사용 완료"; if (compassButton != null) compassButton.interactable = false; if (compassIcon != null) compassIcon.color = iconInactiveColor; SetHintAlertVisible(false); } public void ShowMessage(string message) { ShowGuideMessage(message); } public void ShowGuideMessage(string message) { if (guideRoutine != null) StopCoroutine(guideRoutine); guideRoutine = StartCoroutine(GuideMessageRoutine(message)); } private IEnumerator GuideMessageRoutine(string message) { if (guidePanel != null) guidePanel.SetActive(true); if (guideText != null) { guideText.text = message; guideText.color = normalColor; } if (guideCanvasGroup != null) { yield return FadeCanvasGroup(guideCanvasGroup, 0f, 1f, fadeTime); yield return new WaitForSeconds(guideShowTime); yield return FadeCanvasGroup(guideCanvasGroup, 1f, 0f, fadeTime); } else { yield return new WaitForSeconds(guideShowTime); } if (guidePanel != null) guidePanel.SetActive(false); guideRoutine = null; } private IEnumerator FadeCanvasGroup(CanvasGroup canvasGroup, float from, float to, float duration) { if (canvasGroup == null) yield break; if (duration <= 0f) { canvasGroup.alpha = to; yield break; } float timer = 0f; while (timer < duration) { timer += Time.deltaTime; float t = Mathf.Clamp01(timer / duration); canvasGroup.alpha = Mathf.Lerp(from, to, t); yield return null; } canvasGroup.alpha = to; } public void ShowSuccessResult(int reachedCheckpointCount, int totalCheckpointCount) { ShowSuccessResult(successResultTitleMessage, successResultBodyMessage, true, reachedCheckpointCount, totalCheckpointCount); } public void ShowSuccessResult(string titleMessage, string bodyMessage, bool showMemoryPieceReward, int reachedCheckpointCount, int totalCheckpointCount) { ShowResult(titleMessage, bodyMessage, resultSuccessTitleColor, showMemoryPieceReward, reachedCheckpointCount, totalCheckpointCount); } public void SetResultMemoryPieceState(bool hasMemoryPiece) { if (resultRewardGlow != null) resultRewardGlow.SetActive(hasMemoryPiece); if (resultMemoryCrystalIcon != null) { if (hideResultMemoryCrystalWhenMissing) resultMemoryCrystalIcon.gameObject.SetActive(hasMemoryPiece); else { resultMemoryCrystalIcon.gameObject.SetActive(true); resultMemoryCrystalIcon.color = hasMemoryPiece ? iconActiveColor : iconInactiveColor; } } } public void SetResultCheckpointProgress(int reachedCount, int totalCount) { SetStarProgress(resultCheckpointStarIcons, reachedCount, totalCount, hideInactiveResultCheckpointStars); } public void ShowSuccess() { ShowSuccess(successResultTitleMessage, successResultBodyMessage); } public void ShowSuccess(string bodyMessage) { ShowSuccess(successResultTitleMessage, bodyMessage); } public void ShowSuccess(string titleMessage, string bodyMessage) { ShowResult(titleMessage, bodyMessage, resultSuccessTitleColor, true, -1, -1); } public void ShowFail() { ShowFail(failResultBodyMessage); } public void ShowFail(string bodyMessage) { ShowResult(failResultTitleMessage, bodyMessage, resultFailTitleColor, false, 0, GetResultStarIconCount()); } private void ShowResult(string titleMessage, string bodyMessage, Color titleColor, bool showMemoryPieceReward, int reachedCheckpointCount, int totalCheckpointCount) { HideHUD(); HideGuideImmediately(); SetResultButtonLabels(); if (resultPanel != null) resultPanel.SetActive(true); if (placeResultInFrontWhenShown && panelFollower != null) panelFollower.PlaceResultInFrontNow(); if (resultTitleText != null) { resultTitleText.text = titleMessage; resultTitleText.color = titleColor; } if (resultBodyText != null) resultBodyText.text = bodyMessage; SetResultMemoryPieceState(showMemoryPieceReward); if (reachedCheckpointCount >= 0 && totalCheckpointCount >= 0) SetResultCheckpointProgress(reachedCheckpointCount, totalCheckpointCount); } public void SetResultButtonLabels() { if (retryButtonText != null) retryButtonText.text = retryButtonLabel; if (exitButtonText != null) exitButtonText.text = exitButtonLabel; } public void HideResult() { if (resultPanel != null) resultPanel.SetActive(false); SetResultMemoryPieceState(false); SetResultCheckpointProgress(0, GetResultStarIconCount()); } public void HideReward() { HideResult(); } public void HideGuideImmediately() { if (guideRoutine != null) { StopCoroutine(guideRoutine); guideRoutine = null; } if (guideCanvasGroup != null) guideCanvasGroup.alpha = 0f; if (guidePanel != null) guidePanel.SetActive(false); } public void ResetUI(bool hasCompass) { ShowHUD(); HideResult(); HideGuideImmediately(); SetStartObjective(); SetMemoryPieceState(false); int checkpointIconCount = checkpointStarIcons != null ? checkpointStarIcons.Length : 0; SetCheckpointProgress(0, checkpointIconCount); if (hasCompass) SetCompassAvailable(); else SetCompassNone(); } public void OnMemoryPieceCollected() { SetMemoryPieceState(true); SetObjectiveReturningToExit(); ShowGuideMessage("기억의 조각을 찾았다!\n이제 출구로 돌아가자."); } public void OnCompassUsedToMemory() { SetCompassUsed(); ShowGuideMessage("나침반이 기억의 조각 방향을 가리킨다.\n빛나는 발자국을 따라가 보자."); } public void OnCompassUsedToExit() { SetCompassUsed(); ShowGuideMessage("나침반이 출구 방향을 가리킨다.\n발자국을 따라 출구로 돌아가자."); } public void OnCompassUsedToNextPath() { SetCompassUsed(); ShowGuideMessage("나침반이 다음 길을 가리킨다.\n빛나는 발자국을 따라가 보자."); } public void OnCompassMissing() { ShowGuideMessage("낡은 나침반이 없다."); } public void OnCompassAlreadyUsed() { ShowGuideMessage("나침반은 이미 사용했다."); } private void SetStarProgress(Image[] starIcons, int reachedCount, int totalCount, bool hideInactiveStars) { if (starIcons == null) return; int safeTotal = Mathf.Clamp(totalCount, 0, starIcons.Length); int safeReached = Mathf.Clamp(reachedCount, 0, safeTotal); for (int i = 0; i < starIcons.Length; i++) { Image starIcon = starIcons[i]; if (starIcon == null) continue; bool slotUsed = i < safeTotal; bool reached = i < safeReached; if (!slotUsed) { starIcon.gameObject.SetActive(false); continue; } if (hideInactiveStars) { starIcon.gameObject.SetActive(reached); if (reached) starIcon.color = iconActiveColor; } else { starIcon.gameObject.SetActive(true); starIcon.color = reached ? iconActiveColor : iconInactiveColor; } } } private int GetResultStarIconCount() { return resultCheckpointStarIcons != null ? resultCheckpointStarIcons.Length : 0; } #if UNITY_EDITOR private void OnValidate() { guideShowTime = Mathf.Max(0f, guideShowTime); fadeTime = Mathf.Max(0f, fadeTime); if (checkpointStarIcons != null && checkpointStarIcons.Length == 0) Debug.LogWarning("MazeUIManager: HUD Checkpoint Star Icons 배열이 비어 있습니다. HUD 체크포인트 별 아이콘을 연결하세요.", this); if (resultCheckpointStarIcons != null && resultCheckpointStarIcons.Length == 0) Debug.LogWarning("MazeUIManager: Result Checkpoint Star Icons 배열이 비어 있습니다. 결과창 체크포인트 별 아이콘을 연결하세요.", this); if (placeResultInFrontWhenShown && panelFollower == null) Debug.LogWarning("MazeUIManager: Place Result In Front When Shown이 켜져 있지만 Panel Follower가 비어 있습니다. 결과창 위치가 한 프레임 늦게 잡힐 수 있습니다.", this); } #endif }