356 lines
17 KiB
C#
356 lines
17 KiB
C#
#nullable enable
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UVC.Locale;
|
|
using UVC.Util; // LocalizationManager를 사용한다면 필요합니다.
|
|
|
|
namespace UVC.UI.Tooltip
|
|
{
|
|
/// <summary>
|
|
/// 툴팁 UI를 전역적으로 관리하는 싱글톤 클래스입니다.
|
|
/// 툴팁의 생성, 표시, 숨김, 위치 조정 및 다국어 지원 기능을 담당합니다.
|
|
/// 이 클래스는 일반적으로 애플리케이션 전체에서 하나의 인스턴스만 존재합니다.
|
|
///
|
|
/// 사용 예시:
|
|
/// 1. 초기화: 애플리케이션 시작 시점 (예: GameManager, UIManager 등)에서 한 번 호출해야 합니다.
|
|
/// <code>
|
|
/// // Canvas Transform과 최상위 Canvas 객체를 전달하여 초기화합니다.
|
|
/// // TooltipManager.Instance.Initialize(uiCanvas.transform, uiCanvas);
|
|
/// </code>
|
|
///
|
|
/// 2. 툴팁 표시/숨김 요청: TooltipHandler를 통해 간접적으로 호출되거나, 직접 호출할 수도 있습니다.
|
|
/// TooltipHandler를 사용하는 것이 일반적입니다.
|
|
/// </summary>
|
|
public class TooltipManager
|
|
{
|
|
#region Singleton Implementation
|
|
private static TooltipManager _instance;
|
|
public static TooltipManager Instance
|
|
{
|
|
get
|
|
{
|
|
if (_instance == null)
|
|
{
|
|
_instance = new TooltipManager();
|
|
}
|
|
return _instance;
|
|
}
|
|
}
|
|
// 외부에서 직접 생성하지 못하도록 protected 생성자로 변경
|
|
protected TooltipManager() { }
|
|
#endregion
|
|
|
|
protected Transform _defaultParentTransform; // 툴팁 인스턴스가 생성될 기본 부모 Transform
|
|
protected Canvas canvas; // 화면 좌표 계산 및 UI 스케일링에 사용될 Canvas
|
|
|
|
protected GameObject _activeTooltipInstance; // 현재 활성화된 툴팁 게임 오브젝트
|
|
protected TextMeshProUGUI _tooltipTextElement; // 툴팁 텍스트를 표시하는 TextMeshProUGUI 컴포넌트
|
|
protected RectTransform _tooltipRectTransform; // 툴팁의 크기 및 위치 조정을 위한 RectTransform
|
|
|
|
protected bool _isInitialized = false; // 초기화 완료 여
|
|
|
|
/// <summary>
|
|
/// TooltipManager가 성공적으로 초기화되었는지 여부를 반환합니다.
|
|
/// </summary>
|
|
public bool IsInitialized => _isInitialized;
|
|
|
|
protected string tooltipPrefabPath = "Prefabs/UI/Tooltip/Tooltip";
|
|
|
|
protected GameObject tooltipPrefab;
|
|
|
|
/// <summary>
|
|
/// Resources 폴더 내의 툴팁 UI 프리팹 경로입니다.
|
|
/// 상속을 통해 다른 경로를 사용하도록 재정의할 수 있습니다.
|
|
/// </summary>
|
|
protected virtual string TooltipPrefabPath => tooltipPrefabPath;
|
|
|
|
/// <summary>
|
|
/// TooltipManager를 초기화합니다. 이 메서드는 한 번만 호출되어야 합니다.
|
|
/// 일반적으로 애플리케이션 시작 시점이나 UI 시스템이 로드될 때 호출됩니다.
|
|
/// 툴팁 프리팹을 로드하고, 필요한 컴포넌트를 찾아 내부 변수에 할당합니다.
|
|
/// </summary>
|
|
/// <param name="defaultParent">생성된 툴팁 인스턴스의 기본 부모 Transform입니다. 보통 Canvas의 Transform입니다.</param>
|
|
/// <param name="rootCanvas">툴팁의 위치와 크기를 계산하는 데 사용될 최상위 Canvas입니다.</param>
|
|
/// <param name="customTooltipPrefabPath">Resources 폴더 내의 사용자 정의 툴팁 UI 프리팹 경로입니다. null일 경우 기본 경로를 사용합니다.</param>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 게임 시작 시 UIManager 등에서 호출
|
|
/// public class UIManager : MonoBehaviour
|
|
/// {
|
|
/// public Canvas canvas; // Inspector에서 할당
|
|
///
|
|
/// void Start()
|
|
/// {
|
|
/// // canvas.transform을 부모로, mainCanvas를 루트 캔버스로 하여 초기화
|
|
/// TooltipManager.Instance.Initialize(canvas.transform);
|
|
///
|
|
/// // 특정 프리팹 경로를 사용하고 싶다면:
|
|
/// // TooltipManager.Instance.Initialize(canvas.transform, "MyCustomTooltipPrefab");
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public void Initialize(Transform? defaultParent = null, string? tooltipPrefabPath = null)
|
|
{
|
|
if (_isInitialized)
|
|
{
|
|
Debug.LogWarning("TooltipVisualManager는 이미 초기화되었습니다.");
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(tooltipPrefabPath))
|
|
{
|
|
this.tooltipPrefabPath = tooltipPrefabPath; // 사용자 지정 경로가 제공되면 업데이트
|
|
}
|
|
|
|
if (tooltipPrefab == null)
|
|
{
|
|
tooltipPrefab = Resources.Load<GameObject>(this.tooltipPrefabPath);
|
|
}
|
|
|
|
if (tooltipPrefab == null)
|
|
{
|
|
Debug.LogError($"TooltipManager 초기화 실패: Resources 폴더에서 '{this.tooltipPrefabPath}' 경로의 프리팹을 로드할 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
canvas = CanvasUtil.GetOrCreate("ModalCanvas");
|
|
defaultParent ??= canvas.transform; // 기본 부모가 null인 경우, 새로 생성한 Canvas의 Transform을 사용
|
|
|
|
_defaultParentTransform = defaultParent;
|
|
|
|
|
|
// 툴팁 인스턴스 생성 및 초기화
|
|
_activeTooltipInstance = GameObject.Instantiate(tooltipPrefab, _defaultParentTransform);
|
|
_tooltipTextElement = _activeTooltipInstance.GetComponentInChildren<TextMeshProUGUI>();
|
|
_tooltipRectTransform = _activeTooltipInstance.GetComponent<RectTransform>();
|
|
|
|
if (_tooltipTextElement == null || _tooltipRectTransform == null)
|
|
{
|
|
Debug.LogError("TooltipVisualManager 초기화 실패: tooltipPrefab의 구성요소가 올바르지 않습니다. TextMeshProUGUI와 RectTransform이 필요합니다.");
|
|
if (_activeTooltipInstance != null) GameObject.Destroy(_activeTooltipInstance);
|
|
_activeTooltipInstance = null; // 초기화 실패 상태로 설정
|
|
return;
|
|
}
|
|
|
|
_activeTooltipInstance.SetActive(false); // 처음에는 숨김
|
|
_isInitialized = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 마우스 포인터가 UI 요소에 진입했을 때 호출되어 툴팁을 표시합니다.
|
|
/// TooltipHandler 컴포넌트에 의해 주로 호출됩니다.
|
|
/// 툴팁 내용은 다국어 처리를 거쳐 표시될 수 있습니다.
|
|
/// </summary>
|
|
/// <param name="tooltipKeyOrText">표시할 툴팁 내용 또는 다국어 키입니다.</param>
|
|
/// <param name="mousePosition">현재 마우스 포인터의 화면 좌표입니다.</param>
|
|
public void HandlePointerEnter(string tooltipKeyOrText, Vector3 mousePosition)
|
|
{
|
|
if (!_isInitialized || _activeTooltipInstance == null)
|
|
{
|
|
// Debug.LogWarning("TooltipManager가 초기화되지 않았거나 툴팁 인스턴스가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
string tooltipTextToShow = tooltipKeyOrText; // 기본값은 전달된 문자열 자체
|
|
|
|
// LocalizationManager가 있고, tooltipKeyOrText가 비어있지 않다면 다국어 시도
|
|
if (LocalizationManager.Instance != null && !string.IsNullOrEmpty(tooltipKeyOrText))
|
|
{
|
|
string localizedText = LocalizationManager.Instance.GetString(tooltipKeyOrText);
|
|
// 번역된 텍스트가 있고, 원본 키와 다르다면 (즉, 번역 성공) 해당 텍스트 사용
|
|
if (!string.IsNullOrEmpty(localizedText) && localizedText != tooltipKeyOrText)
|
|
{
|
|
tooltipTextToShow = localizedText;
|
|
}
|
|
// 번역 결과가 비어있다면 (키는 있지만 번역 값이 없는 경우) 툴팁을 숨김
|
|
else if (string.IsNullOrEmpty(localizedText))
|
|
{
|
|
HideTooltip();
|
|
return;
|
|
}
|
|
// 그 외의 경우 (키 자체가 번역 값인 경우 등)는 tooltipKeyOrText를 그대로 사용
|
|
}
|
|
|
|
// 최종적으로 표시할 텍스트가 비어있다면 툴팁을 숨김
|
|
if (string.IsNullOrEmpty(tooltipTextToShow))
|
|
{
|
|
HideTooltip();
|
|
return;
|
|
}
|
|
ShowTooltip(tooltipTextToShow, mousePosition);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 마우스 포인터가 UI 요소에서 벗어났을 때 호출되어 툴팁을 숨깁니다.
|
|
/// TooltipHandler 컴포넌트에 의해 주로 호출됩니다.
|
|
/// </summary>
|
|
public void HandlePointerExit()
|
|
{
|
|
// ULog.Debug("TooltipManager.HandlePointerExit() called."); // 디버그 로그 필요시 활성화
|
|
if (!_isInitialized) return;
|
|
HideTooltip();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 툴팁 UI를 화면에 실제로 표시하고 내용을 설정하며 위치를 조정합니다.
|
|
/// </summary>
|
|
/// <param name="text">툴팁에 표시될 최종 텍스트입니다.</param>
|
|
/// <param name="mousePosition">툴팁 위치를 결정하기 위한 현재 마우스 포인터의 화면 좌표입니다.</param>
|
|
private void ShowTooltip(string text, Vector3 mousePosition)
|
|
{
|
|
if (_activeTooltipInstance == null || _tooltipTextElement == null || _tooltipRectTransform == null) return;
|
|
|
|
_tooltipTextElement.text = text; // 텍스트 설정
|
|
_activeTooltipInstance.SetActive(true); // 툴팁 활성화
|
|
|
|
// 툴팁을 현재 부모 내에서 가장 첫번쨰 자식으로 만들어 다른 UI 요소들 위에 표시되도록 합니다.
|
|
if (_activeTooltipInstance.transform.parent != null)
|
|
{
|
|
_activeTooltipInstance.transform.SetAsFirstSibling();
|
|
}
|
|
|
|
_tooltipTextElement.ForceMeshUpdate(); // 텍스트 변경 후 메쉬 강제 업데이트 (정확한 크기 계산 위함)
|
|
Vector2 textSize = _tooltipTextElement.GetPreferredValues(text); // 텍스트 내용에 따른 선호 크기 계산
|
|
Vector2 padding = new Vector2(10, 2); // 툴팁 내부 여백 (좌우 10, 상하 2)
|
|
_tooltipRectTransform.sizeDelta = textSize + padding * 2; // 텍스트 크기와 여백을 합쳐 툴팁 전체 크기 설정
|
|
|
|
AdjustPosition(mousePosition); // 마우스 위치 기준으로 툴팁 위치 조정
|
|
}
|
|
|
|
/// <summary>
|
|
/// 툴팁의 위치를 마우스 포인터 주변으로 조정합니다.
|
|
/// 화면 좌표계의 마우스 위치를 Canvas 내의 로컬 좌표로 변환하여 사용합니다.
|
|
/// </summary>
|
|
/// <param name="mousePosition">현재 마우스 포인터의 화면 좌표입니다.</param>
|
|
private void AdjustPosition(Vector3 mousePosition)
|
|
{
|
|
if (canvas == null || _tooltipRectTransform == null) return;
|
|
|
|
Vector2 localPoint; // Canvas 내 로컬 좌표
|
|
// 현재 Canvas의 Render Mode에 따라 적절한 카메라 사용
|
|
Camera eventCamera = (canvas.renderMode == RenderMode.ScreenSpaceOverlay) ? null : canvas.worldCamera;
|
|
|
|
// 화면 좌표(mousePosition)를 _rootCanvas의 RectTransform 내 로컬 좌표로 변환
|
|
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
|
canvas.transform as RectTransform, // 좌표 변환의 기준이 될 RectTransform
|
|
mousePosition, // 변환할 화면 좌표
|
|
eventCamera, // 이벤트 카메라 (ScreenSpaceOverlay의 경우 null)
|
|
out localPoint // 변환된 로컬 좌표 결과
|
|
);
|
|
|
|
// 툴팁을 마우스 오른쪽 아래에 표시하기 위한 오프셋 계산.
|
|
// 툴팁 RectTransform의 Pivot 설정을 고려합니다.
|
|
// 예: Pivot (0,1) (좌상단)일 경우, 마우스 위치에서 오른쪽으로 약간, 아래로 툴팁 높이만큼 이동.
|
|
Vector2 pivotOffset = new Vector2(
|
|
_tooltipRectTransform.sizeDelta.x * _tooltipRectTransform.pivot.x + 10f, // X 오프셋 (오른쪽으로 10px)
|
|
_tooltipRectTransform.sizeDelta.y * (1 - _tooltipRectTransform.pivot.y) - 10f // Y 오프셋 (아래로 10px)
|
|
);
|
|
|
|
// 최종적으로 툴팁의 로컬 위치 설정
|
|
_tooltipRectTransform.localPosition = localPoint + pivotOffset;
|
|
|
|
// 화면 경계를 벗어나지 않도록 위치 추가 조정
|
|
AdjustPositionWithinScreenBounds();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 활성화된 툴팁 인스턴스를 비활성화하여 화면에서 숨깁니다.
|
|
/// </summary>
|
|
public void HideTooltip()
|
|
{
|
|
if (_activeTooltipInstance != null)
|
|
{
|
|
_activeTooltipInstance.SetActive(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 툴팁이 화면 경계를 벗어나는 경우 위치를 조정하여 화면 내에 보이도록 합니다.
|
|
/// </summary>
|
|
private void AdjustPositionWithinScreenBounds()
|
|
{
|
|
if (_tooltipRectTransform == null || _activeTooltipInstance == null || !_activeTooltipInstance.activeSelf || canvas == null) return;
|
|
|
|
Vector3[] tooltipCorners = new Vector3[4]; // 툴팁의 네 꼭짓점 월드 좌표
|
|
_tooltipRectTransform.GetWorldCorners(tooltipCorners);
|
|
|
|
RectTransform canvasRectTransform = canvas.transform as RectTransform;
|
|
|
|
// 화면 경계 좌표 설정
|
|
float minX, maxX, minY, maxY;
|
|
|
|
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
|
|
{
|
|
// Screen Space Overlay 모드에서는 Screen.width/height를 사용
|
|
minX = 0;
|
|
maxX = Screen.width;
|
|
minY = 0;
|
|
maxY = Screen.height;
|
|
}
|
|
else
|
|
{
|
|
// Screen Space Camera 또는 World Space 모드에서는 Canvas의 월드 좌표 경계를 사용
|
|
Vector3[] canvasCorners = new Vector3[4];
|
|
canvasRectTransform.GetWorldCorners(canvasCorners);
|
|
minX = canvasCorners[0].x; // Bottom-left X
|
|
maxX = canvasCorners[2].x; // Top-right X
|
|
minY = canvasCorners[0].y; // Bottom-left Y
|
|
maxY = canvasCorners[1].y; // Top-left Y (또는 corners[2].y)
|
|
}
|
|
|
|
Vector3 currentPosition = _tooltipRectTransform.position; // 현재 툴팁 위치 (월드 좌표)
|
|
Vector2 size = _tooltipRectTransform.sizeDelta * canvas.scaleFactor; // Canvas 스케일을 고려한 실제 픽셀 크기
|
|
Vector2 pivot = _tooltipRectTransform.pivot; // 툴팁의 Pivot
|
|
|
|
// 오른쪽 경계 넘어감: 왼쪽으로 이동
|
|
if (tooltipCorners[2].x > maxX) // Top-right X
|
|
{
|
|
currentPosition.x -= (tooltipCorners[2].x - maxX);
|
|
}
|
|
// 왼쪽 경계 넘어감: 오른쪽으로 이동
|
|
if (tooltipCorners[0].x < minX) // Bottom-left X
|
|
{
|
|
currentPosition.x += (minX - tooltipCorners[0].x);
|
|
}
|
|
// 아래쪽 경계 넘어감: 마우스 포인터 위쪽으로 이동
|
|
if (tooltipCorners[0].y < minY) // Bottom-left Y
|
|
{
|
|
Vector3 mouseWorldPos = Vector3.zero;
|
|
// 마우스 포인터의 월드 Y 좌표를 가져옴
|
|
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay) mouseWorldPos = Input.mousePosition;
|
|
else RectTransformUtility.ScreenPointToWorldPointInRectangle(canvasRectTransform, Input.mousePosition, canvas.worldCamera, out mouseWorldPos);
|
|
|
|
// 마우스 Y 위치 + 툴팁 높이 (pivot 고려) + 약간의 오프셋
|
|
currentPosition.y = mouseWorldPos.y + (size.y * (1 - pivot.y)) + 15f;
|
|
}
|
|
// 위쪽 경계 넘어감: 아래로 이동
|
|
if (tooltipCorners[1].y > maxY) // Top-left Y
|
|
{
|
|
currentPosition.y -= (tooltipCorners[1].y - maxY);
|
|
}
|
|
_tooltipRectTransform.position = currentPosition; // 조정된 위치 적용
|
|
}
|
|
|
|
/// <summary>
|
|
/// TooltipManager가 사용하던 리소스를 해제합니다.
|
|
/// 애플리케이션 종료 시 또는 UI 시스템 해제 시 호출하는 것이 좋습니다.
|
|
/// 생성된 툴팁 인스턴스를 파괴하고 내부 참조들을 null로 설정합니다.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (_activeTooltipInstance != null)
|
|
{
|
|
GameObject.Destroy(_activeTooltipInstance);
|
|
_activeTooltipInstance = null;
|
|
}
|
|
_tooltipTextElement = null;
|
|
_tooltipRectTransform = null;
|
|
_defaultParentTransform = null;
|
|
canvas = null;
|
|
_isInitialized = false;
|
|
_instance = null; // 싱글톤 인스턴스 참조 해제
|
|
}
|
|
}
|
|
} |