Unity UI Toolkit을 활용한 반응형 인벤토리 툴팁 및 아이템 비교 UI 구축

Unity UI Toolkit의 Flexbox 레이아웃과 이벤트 시스템을 활용해 화면 이탈을 방지하는 반응형 툴팁과 장착 아이템 수치 비교 UI를 구현하는 방법을 다룹니다.

✓ 복사 완료!
챕터게임플레이 로직
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
Unity UI Toolkit을 활용한 반응형 인벤토리 툴팁 및 아이템 비교 UI 구축

UI Toolkit 기반 툴팁 시스템의 구현 원리

유니티(Unity)의 UI Toolkit은 Flexbox 기반의 레이아웃 엔진과 웹 표준과 유사한 UXML 및 USS 구조를 지원합니다. 기존 uGUI 방식에서는 툴팁의 크기가 가변적일 때 캔버스 래퍼 앵커와 Pivot을 직접 재계산해야 하는 번거로움이 있었습니다. 반면 UI Toolkit에서는 VisualElement의 자동 레이아웃 연산과 스타일 적용을 활용하여 직관적이고 반응성이 뛰어난 UI를 구현할 수 있습니다.

복잡한 RPG 인벤토리 툴팁은 단순히 아이템의 이름과 설명을 보여주는 것을 넘어 장착 중인 아이템과의 스탯 비교 기능도 함께 수행해야 합니다. 이를 위해 툴팁 메인 컨테이너 내부에 비교 전용 패널을 배치하고 마우스 호버 이벤트 발생 시 두 아이템의 데이터를 비교하여 동적으로 텍스트와 스타일을 업데이트하는 구조를 설계합니다.

인벤토리 아이템 호버 시 나타나는 툴팁 및 비교 UI 구조

툴팁 위치 계산과 화면 이탈 방지

툴팁은 항상 커서 근처에 나타나지만 화면 가장자리에 커서가 위치할 경우 툴팁 일부가 패널 밖으로 삐져나가는 클리핑 문제가 발생합니다. 이를 방지하려면 현재 커서 위치, 툴팁 요소의 실제 렌더링 크기 그리고 패널 전체의 경계를 비교하여 좌표를 제한(Clamping)해야 합니다.

툴팁의 최종 좌표는 다음과 같은 공식으로 계산할 수 있습니다.

xclamped=min⁡(max⁡(xpointer+offsetx,0),Wpanel−Wtooltip)x_{clamped} = \min(\max(x_{pointer} + offset_x, 0), W_{panel} - W_{tooltip}) yclamped=min⁡(max⁡(ypointer+offsety,0),Hpanel−Htooltip)y_{clamped} = \min(\max(y_{pointer} + offset_y, 0), H_{panel} - H_{tooltip})

이 공식은 마우스 커서 위치에 오프셋을 더한 값이 패널의 좌측 상단 범위를 벗어나지 않도록 하고 우측 하단을 넘어갈 경우 툴팁이 패널 안쪽으로 밀려 들어오도록 좌표를 조정해 줍니다.

아래 다이어그램은 툴팁 표시 및 위치 연산 흐름을 나타냅니다.

flowchart TD
    A[PointerOverEvent 발생] --> B[아이템 데이터 및 장착 아이템 데이터 조회]
    B --> C[VisualElement 바인딩 및 비교 텍스트 생성]
    C --> D[PointerMoveEvent 기반 마우스 좌표 수집]
    D --> E[화면 경계 및 Tooltip 크기 계산]
    E --> F[위치 Clamping 및 style.left / style.top 업데이트]
    F --> G[Tooltip Display: Flex 전환]

마우스 이동 이벤트가 발생할 때마다 실시간으로 클리핑 연산을 수행하면 다양한 해상도와 창 크기에서도 툴팁이 화면 밖으로 잘리지 않습니다.

아이템 비교 데이터 바인딩

비교 툴팁의 핵심은 비교 대상 아이템과 현재 장착 중인 아이템 간의 수치 차이를 한눈에 알아볼 수 있도록 시각화하는 것입니다. 공격력, 방어력, 크리티컬 확률 등의 스탯 차이 값을 구한 뒤 양수와 음수에 따라 다른 USS 클래스를 동적으로 부여합니다.

  • 증가 스탯: 긍정적 스타일 .stat-diff--positive (녹색 텍스트 및 + 기호)
  • 감소 스탯: 부정적 스타일 .stat-diff--negative (빨간색 텍스트 및 - 기호)
  • 동일 스탯: 중립 스타일 .stat-diff--neutral

장착 아이템과 비교 대상 아이템의 스탯 차이가 색상별로 표시된 툴팁 UI

스탯 목록을 순회하면서 차이값을 라벨에 채워 넣고 미리 정의한 USS 클래스를 추가하면 복잡한 색상 분기 로직 없이도 깔끔하게 비주얼을 제어할 수 있습니다.

C# 툴팁 컨트롤러 실전 코드 구현

아래는 UI Toolkit에서 커스텀 툴팁과 비교 UI를 제어하는 InventoryTooltipController 핵심 C# 코드 예시입니다.

using UnityEngine;
using UnityEngine.UIElements;

public class InventoryTooltipController : MonoBehaviour
{
    [SerializeField] private UIDocument uiDocument;
    
    private VisualElement _tooltipRoot;
    private Label _itemNameLabel;
    private VisualElement _comparisonContainer;
    private Vector2 _offset = new Vector2(15f, 15f);

    private void OnEnable()
    {
        var root = uiDocument.rootVisualElement;
        _tooltipRoot = root.Q<VisualElement>("inventory-tooltip");
        _itemNameLabel = root.Q<Label>("tooltip-item-name");
        _comparisonContainer = root.Q<VisualElement>("comparison-container");

        // 툴팁 요소 자체는 마우스 이벤트를 차단해야 마우스 깜빡임 현상이 방지됩니다.
        _tooltipRoot.pickingMode = PickingMode.Ignore;
        HideTooltip();
    }

    public void ShowTooltip(ItemData hoveredItem, ItemData equippedItem, Vector2 pointerPosition)
    {
        _itemNameLabel.text = hoveredItem.ItemName;
        UpdateComparisonStats(hoveredItem, equippedItem);

        _tooltipRoot.style.display = DisplayStyle.Flex;
        SetTooltipPosition(pointerPosition);
    }

    public void UpdatePosition(Vector2 pointerPosition)
    {
        if (_tooltipRoot.style.display == DisplayStyle.Flex)
        {
            SetTooltipPosition(pointerPosition);
        }
    }

    public void HideTooltip()
    {
        _tooltipRoot.style.display = DisplayStyle.None;
    }

    private void SetTooltipPosition(Vector2 pointerPosition)
    {
        VisualElement panelRoot = _tooltipRoot.panel.visualTree;
        
        float panelWidth = panelRoot.layout.width;
        float panelHeight = panelRoot.layout.height;
        
        float tooltipWidth = _tooltipRoot.layout.width;
        float tooltipHeight = _tooltipRoot.layout.height;

        float targetX = pointerPosition.x + _offset.x;
        float targetY = pointerPosition.y + _offset.y;

        // 화면 이탈 방지 Clamping 연산
        float clampedX = Mathf.Clamp(targetX, 0f, Mathf.Max(0f, panelWidth - tooltipWidth));
        float clampedY = Mathf.Clamp(targetY, 0f, Mathf.Max(0f, panelHeight - tooltipHeight));

        _tooltipRoot.style.left = clampedX;
        _tooltipRoot.style.top = clampedY;
    }

    private void UpdateComparisonStats(ItemData hovered, ItemData equipped)
    {
        _comparisonContainer.Clear();
        if (equipped == null) return;

        foreach (var stat in hovered.Stats)
        {
            float equippedValue = equipped.GetStatValue(stat.Type);
            float diff = stat.Value - equippedValue;

            var row = new VisualElement();
            row.AddToClassList("stat-row");

            var nameLabel = new Label($"{stat.Type}: {stat.Value}");
            var diffLabel = new Label();

            if (diff > 0)
            {
                diffLabel.text = $"(+{diff})";
                diffLabel.AddToClassList("stat-diff--positive");
            }
            else if (diff < 0)
            {
                diffLabel.text = $"({diff})";
                diffLabel.AddToClassList("stat-diff--negative");
            }

            row.Add(nameLabel);
            row.Add(diffLabel);
            _comparisonContainer.Add(row);
        }
    }
}

성능 최적화 및 레이아웃 주의사항

UI Toolkit으로 인벤토리 툴팁 시스템을 구축할 때 자주 발생하는 문제와 해결 방안은 다음과 같습니다.

  1. PickingMode 설정 필수: 툴팁 요소의 pickingMode를 PickingMode.Ignore로 설정하지 않으면 툴팁이 마우스 커서를 가리는 순간 PointerLeaveEvent가 발생하여 툴팁이 깜빡이는 현상이 나타납니다.
  2. Garbage Collection 최소화: 마우스가 이동할 때마다 새로운 스타일이나 클래스를 할당하는 대신 툴팁 요소를 캐싱하고 재사용해야 메모리 누수와 프레임 드랍을 막을 수 있습니다.
  3. 크기 측정 시점(GeometryChangedEvent): 툴팁 내용이 바뀐 직후 레이아웃 수치는 이전 프레임의 크기일 수 있습니다. 처음 활성화되는 시점에는 GeometryChangedEvent를 수신하거나 첫 프레임 연산을 보정해 정확한 툴팁 크기 기준 좌표 계산을 수행해야 합니다.

이러한 요소들을 종합적으로 고려해 설계하면 다양한 화면 비율에서도 안정적으로 동작하는 인벤토리 툴팁 UI를 완성할 수 있습니다.