using TMPro; using UnityEngine; using UnityEngine.UI; public class UIManager : MonoBehaviour { [Header("Follow Camera UI")] [SerializeField] private bool followCamera = true; [Tooltip("XR Origin > Camera Offset > Main Camera를 넣으세요.")] [SerializeField] private Transform cameraTarget; [Tooltip("따라다닐 UI 전체 Root입니다. 비워두면 이 UIManager가 붙은 오브젝트를 움직입니다.")] [SerializeField] private Transform followRoot; [Tooltip("카메라 앞 몇 m 위치에 UI를 둘지 정합니다.")] [SerializeField] private float followDistance = 1.3f; [Tooltip("카메라 높이 기준 위/아래 위치 보정값입니다. 양수면 위로, 음수면 아래로 갑니다.")] [SerializeField] private float heightOffset = 0.1f; [Tooltip("카메라 기준 좌우 위치 보정값입니다.")] [SerializeField] private float sideOffset = 0f; [Tooltip("켜면 UI가 부드럽게 따라옵니다.")] [SerializeField] private bool smoothFollow = true; [SerializeField] private float followSpeed = 8f; [Tooltip("켜면 회전도 부드럽게 따라옵니다.")] [SerializeField] private bool smoothRotation = true; [SerializeField] private float rotationSpeed = 8f; [Tooltip("켜면 카메라의 위아래 기울기는 무시하고 좌우 방향만 따라갑니다.")] [SerializeField] private bool useFlatForward = true; [Tooltip("글자가 뒤집혀 보이면 켜세요.")] [SerializeField] private bool flipRotation = false; [Header("Timing UI")] [SerializeField] private Image timingBarFill; [Header("Control UI")] [SerializeField] private TextMeshProUGUI controlText; [Header("Prompt UI")] [Tooltip("CompletePromptText 또는 CollectPromptText를 연결하세요.")] [SerializeField] private TextMeshProUGUI promptText; [Header("Shell UI")] [SerializeField] private TextMeshProUGUI shellStateText; [Header("Movement Arrows")] [SerializeField] private Image upArrow; [SerializeField] private Image downArrow; [SerializeField] private Image leftArrow; [SerializeField] private Image rightArrow; [SerializeField] private Color activeColor = Color.white; [SerializeField] private Color inactiveColor = new Color(1f, 1f, 1f, 0.2f); [Header("Reward Panel")] [SerializeField] private GameObject rewardPanel; [SerializeField] private TextMeshProUGUI rewardText; [SerializeField] private TextMeshProUGUI resultSubText; [Header("Text Colors")] [SerializeField] private Color normalColor = Color.white; [SerializeField] private Color reversedColor = new Color(1f, 0.25f, 0.25f); [SerializeField] private Color openColor = new Color(0.3f, 1f, 0.65f); [SerializeField] private Color closedColor = new Color(1f, 0.25f, 0.25f); private void Awake() { if (followRoot == null) followRoot = transform; if (cameraTarget == null && Camera.main != null) cameraTarget = Camera.main.transform; HideReward(); ResetArrows(); SetControl(true); SetShellState(false); SetShellTimer(0f, false); SetPromptDefault(); } private void LateUpdate() { FollowCamera(); } public void SetControl(bool reversed) { if (controlText == null) return; if (reversed) { controlText.text = "조작 반전 중"; controlText.color = reversedColor; } else { controlText.text = "일반 조작"; controlText.color = normalColor; } } public void SetPromptDefault() { if (promptText == null) return; promptText.text = "조개가 열렸을 때 트리거를 누르세요"; promptText.color = normalColor; } public void SetPromptFarFromShell() { if (promptText == null) return; promptText.text = "조개 가까이 이동하세요"; promptText.color = normalColor; } public void SetPromptNearShell(bool isOpen) { if (promptText == null) return; if (isOpen) { promptText.text = "지금 트리거를 누르세요!"; promptText.color = openColor; } else { promptText.text = "조개가 열릴 때까지 기다리세요"; promptText.color = closedColor; } } public void SetShellTimer(float value01, bool isOpen) { if (timingBarFill == null) return; timingBarFill.fillAmount = Mathf.Clamp01(value01); timingBarFill.color = isOpen ? openColor : closedColor; } public void SetShellState(bool isOpen) { if (shellStateText == null) return; if (isOpen) { shellStateText.text = "조개 상태: 열림"; shellStateText.color = openColor; } else { shellStateText.text = "조개 상태: 닫힘"; shellStateText.color = closedColor; } } public void SetMovementDebug(Vector2 input, Vector2 output) { UpdateArrows(output); } private void UpdateArrows(Vector2 output) { ResetArrows(); if (output.y > 0.1f && upArrow != null) upArrow.color = activeColor; if (output.y < -0.1f && downArrow != null) downArrow.color = activeColor; if (output.x < -0.1f && leftArrow != null) leftArrow.color = activeColor; if (output.x > 0.1f && rightArrow != null) rightArrow.color = activeColor; } private void ResetArrows() { if (upArrow != null) upArrow.color = inactiveColor; if (downArrow != null) downArrow.color = inactiveColor; if (leftArrow != null) leftArrow.color = inactiveColor; if (rightArrow != null) rightArrow.color = inactiveColor; } public void ShowReward(bool success) { if (rewardPanel != null) { rewardPanel.SetActive(true); rewardPanel.transform.SetAsLastSibling(); } else { Debug.LogWarning("[UIManager] RewardPanel이 연결되지 않았습니다."); } if (rewardText != null) { rewardText.gameObject.SetActive(true); rewardText.text = success ? "성공!" : "실패!"; rewardText.color = success ? openColor : closedColor; } else { Debug.LogWarning("[UIManager] RewardText가 연결되지 않았습니다."); } if (resultSubText != null) { resultSubText.gameObject.SetActive(true); resultSubText.text = success ? "기억 조각을 획득했습니다" : "조개가 닫혀 있었습니다"; resultSubText.color = normalColor; } else { Debug.LogWarning("[UIManager] ResultSubText가 연결되지 않았습니다."); } } public void HideReward() { if (rewardPanel != null) rewardPanel.SetActive(false); } private void FollowCamera() { if (!followCamera) return; if (followRoot == null) return; if (cameraTarget == null) { if (Camera.main != null) cameraTarget = Camera.main.transform; else return; } Vector3 forward = cameraTarget.forward; Vector3 right = cameraTarget.right; if (useFlatForward) { forward.y = 0f; right.y = 0f; if (forward.sqrMagnitude < 0.001f) { forward = cameraTarget.parent != null ? cameraTarget.parent.forward : Vector3.forward; forward.y = 0f; } if (right.sqrMagnitude < 0.001f) { right = cameraTarget.parent != null ? cameraTarget.parent.right : Vector3.right; right.y = 0f; } } forward.Normalize(); right.Normalize(); Vector3 targetPosition = cameraTarget.position + forward * followDistance + right * sideOffset; targetPosition.y = cameraTarget.position.y + heightOffset; Quaternion targetRotation = Quaternion.LookRotation(forward, Vector3.up); if (flipRotation) { targetRotation *= Quaternion.Euler(0f, 180f, 0f); } if (smoothFollow) { followRoot.position = Vector3.Lerp( followRoot.position, targetPosition, Time.deltaTime * followSpeed ); } else { followRoot.position = targetPosition; } if (smoothRotation) { followRoot.rotation = Quaternion.Slerp( followRoot.rotation, targetRotation, Time.deltaTime * rotationSpeed ); } else { followRoot.rotation = targetRotation; } } }