#nullable enable using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; using UVC.Core; using UVC.Extention; using UVC.UI.Modal; namespace UVC.Factory.Modal { /// /// 3D 객체를 따라다니며 정보를 표시하는 UI 창입니다. /// 이 컴포넌트는 World Space Canvas 내의 UI 요소에 추가되어야 합니다. /// public class InfoWindow : SingletonScene { [Tooltip("Label 정보 텍스트를 표시할 UI 요소")] [SerializeField] private TextMeshProUGUI text; [Tooltip("정보 창을 닫는 버튼")] [SerializeField] private Button closeButton; [Tooltip("정보 창의 내용을 클립보드에 복사할 버튼")] [SerializeField] private Button copyButton; [Tooltip("UI가 객체를 가리지 않도록 할 월드 좌표계 오프셋")] [SerializeField] private Vector2 screenOffset = new Vector2(10f, 10f); [Tooltip("UI가 객체를 가리지 않도록 할 메뉴바 높이")] [SerializeField] private float menuBarHeight = 70f; // 정보 창이 따라다닐 3D 객체의 Transform private Transform? target; private string message = string.Empty; /// /// 정보 창이 현재 화면에 표시되고 있는지 여부를 반환합니다. /// public bool IsVisible => gameObject.activeSelf; private RectTransform? rectTransform; protected override void Awake() { base.Awake(); rectTransform = GetComponent(); // 닫기 버튼이 할당되었으면 클릭 이벤트를 연결합니다. if (closeButton != null) { closeButton.onClick.AddListener(Hide); } if (copyButton != null) { copyButton.onClick.AddListener(CopyToClipboard); } // 처음에는 정보 창을 숨깁니다. if (gameObject.activeSelf) { Hide(); } } private void CopyToClipboard() { // 클립보드에 현재 메시지를 복사합니다. if (!string.IsNullOrEmpty(message)) { GUIUtility.systemCopyBuffer = message; Toast.Show("정보가 클립보드에 복사되었습니다.", 2f, 0.5f); } } private void LateUpdate() { // target이 설정되어 있고 활성화 상태일 때만 위치와 방향을 업데이트합니다. if (target != null && gameObject.activeSelf && Camera.main != null) { // 타겟의 렌더러 또는 콜라이더를 가져와 화면 상의 크기를 계산 Bounds bounds = new Bounds(target.position, Vector3.one); Renderer? renderer = target.GetComponent(); Collider? collider = target.GetComponent(); // 렌더러가 있으면 렌더러의 바운드를, 없으면 콜라이더의 바운드를 사용 if (renderer != null) { bounds = renderer.bounds; } else if (collider != null) { bounds = collider.bounds; } // 바운드의 오른쪽 지점을 월드 좌표로 계산 Vector3 rightPoint = bounds.center + new Vector3(bounds.extents.x, 0, 0); // 바운드의 오른쪽 지점을 스크린 좌표로 변환 Vector3 screenPosRight = Camera.main.WorldToScreenPoint(rightPoint); // 추가 오프셋 적용 screenPosRight.x += screenOffset.x; screenPosRight.y += screenOffset.y; // 메뉴바 영역 고려 및 화면 밖으로 나가지 않도록 제한 screenPosRight.x = Mathf.Clamp(screenPosRight.x, 0, Screen.width - rectTransform!.rect.width); screenPosRight.y = Mathf.Clamp(screenPosRight.y, rectTransform!.rect.height, Screen.height - menuBarHeight); // RectTransform을 사용하여 UI 위치 설정 // 캔버스의 렌더링 모드에 따라 다르게 처리 Canvas canvas = GetComponentInParent(); if (canvas != null) { if (canvas.renderMode == RenderMode.ScreenSpaceOverlay) { rectTransform.position = screenPosRight; } else if (canvas.renderMode == RenderMode.ScreenSpaceCamera || canvas.renderMode == RenderMode.WorldSpace) { // 스크린 좌표를 캔버스 상의 로컬 좌표로 변환 Vector2 localPoint; RectTransformUtility.ScreenPointToLocalPointInRectangle( canvas.GetComponent(), screenPosRight, canvas.renderMode == RenderMode.ScreenSpaceCamera ? canvas.worldCamera : Camera.main, out localPoint); rectTransform.localPosition = new Vector3(localPoint.x, localPoint.y, rectTransform.localPosition.z); } } // UI가 항상 보이도록 카메라를 향하게 설정 (World Space Canvas인 경우에만 필요) Canvas parentCanvas = GetComponentInParent(); if (parentCanvas != null && parentCanvas.renderMode == RenderMode.WorldSpace) { transform.LookAt(transform.position + Camera.main.transform.rotation * Vector3.forward, Camera.main.transform.rotation * Vector3.up); } } } /// /// 정보 창을 특정 대상에 대해 표시합니다. /// /// 정보를 표시할 3D 객체의 Transform /// 표시할 정보 문자열 public void Show(Transform targetObject, OrderedDictionary information) { target = targetObject; UpdateInformation(information); gameObject.SetActive(true); // 즉시 위치와 방향을 업데이트합니다. LateUpdate(); } public void UpdateInformation(OrderedDictionary information) { if (target == null) return; if (text != null) { message = string.Empty; string combinedString = string.Empty; foreach (var kvp in information) { // 태그를 사용하여 줄바꿈 시에도 정렬이 유지되도록 합니다. combinedString += $"{kvp.Key}{kvp.Value ?? "null"}\n"; message += $"{kvp.Key}: {kvp.Value ?? "null"}\n"; // 메시지에 추가 } combinedString = combinedString.TrimEnd('\n'); // 마지막 줄바꿈 제거 message = message.TrimEnd('\n'); // 마지막 줄바꿈 제거 text.text = combinedString; } // size를 text에 맞게 조정합니다. RectTransform? rect = GetComponent(); if (rect != null && text != null) { RectTransform textRect = text.GetComponent(); float marginHeight = rect.rect.height - textRect.rect.height; // 상하 여백 rect.sizeDelta = new Vector2(rect.rect.width, text.preferredHeight + marginHeight); } } /// /// 정보 창을 숨깁니다. /// public void Hide() { gameObject.SetActive(false); target = null; } } }