207 lines
8.1 KiB
C#
207 lines
8.1 KiB
C#
#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
|
|
{
|
|
/// <summary>
|
|
/// 3D 객체를 따라다니며 정보를 표시하는 UI 창입니다.
|
|
/// 이 컴포넌트는 World Space Canvas 내의 UI 요소에 추가되어야 합니다.
|
|
/// </summary>
|
|
public class InfoWindow : SingletonScene<InfoWindow>
|
|
{
|
|
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// 정보 창이 현재 화면에 표시되고 있는지 여부를 반환합니다.
|
|
/// </summary>
|
|
public bool IsVisible => gameObject.activeSelf;
|
|
|
|
private RectTransform? rectTransform;
|
|
|
|
protected override void Awake()
|
|
{
|
|
base.Awake();
|
|
|
|
rectTransform = GetComponent<RectTransform>();
|
|
|
|
// 닫기 버튼이 할당되었으면 클릭 이벤트를 연결합니다.
|
|
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<Renderer>();
|
|
Collider? collider = target.GetComponent<Collider>();
|
|
|
|
// 렌더러가 있으면 렌더러의 바운드를, 없으면 콜라이더의 바운드를 사용
|
|
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<Canvas>();
|
|
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<RectTransform>(),
|
|
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<Canvas>();
|
|
if (parentCanvas != null && parentCanvas.renderMode == RenderMode.WorldSpace)
|
|
{
|
|
transform.LookAt(transform.position + Camera.main.transform.rotation * Vector3.forward,
|
|
Camera.main.transform.rotation * Vector3.up);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 정보 창을 특정 대상에 대해 표시합니다.
|
|
/// </summary>
|
|
/// <param name="targetObject">정보를 표시할 3D 객체의 Transform</param>
|
|
/// <param name="information">표시할 정보 문자열</param>
|
|
public void Show(Transform targetObject, OrderedDictionary<string, object> information)
|
|
{
|
|
target = targetObject;
|
|
|
|
UpdateInformation(information);
|
|
|
|
gameObject.SetActive(true);
|
|
|
|
// 즉시 위치와 방향을 업데이트합니다.
|
|
LateUpdate();
|
|
}
|
|
|
|
public void UpdateInformation(OrderedDictionary<string, object> information)
|
|
{
|
|
if (target == null) return;
|
|
if (text != null)
|
|
{
|
|
message = string.Empty;
|
|
string combinedString = string.Empty;
|
|
foreach (var kvp in information)
|
|
{
|
|
// <indent> 태그를 사용하여 줄바꿈 시에도 정렬이 유지되도록 합니다.
|
|
combinedString += $"{kvp.Key}<pos=40%><indent=40%>{kvp.Value ?? "null"}</indent>\n";
|
|
message += $"{kvp.Key}: {kvp.Value ?? "null"}\n"; // 메시지에 추가
|
|
}
|
|
combinedString = combinedString.TrimEnd('\n'); // 마지막 줄바꿈 제거
|
|
message = message.TrimEnd('\n'); // 마지막 줄바꿈 제거
|
|
text.text = combinedString;
|
|
}
|
|
// size를 text에 맞게 조정합니다.
|
|
RectTransform? rect = GetComponent<RectTransform>();
|
|
if (rect != null && text != null)
|
|
{
|
|
RectTransform textRect = text.GetComponent<RectTransform>();
|
|
float marginHeight = rect.rect.height - textRect.rect.height; // 상하 여백
|
|
rect.sizeDelta = new Vector2(rect.rect.width, text.preferredHeight + marginHeight);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 정보 창을 숨깁니다.
|
|
/// </summary>
|
|
public void Hide()
|
|
{
|
|
gameObject.SetActive(false);
|
|
target = null;
|
|
}
|
|
}
|
|
} |