본문 바로가기

공부/유니티

[Unity] 툴팁 만들기 - part1. 배치된 툴팁

게임을 개발하다 보면 다양한 종류의 툴팁이 필요하다.

하지만 이런 툴팁을 필요할 때 마다 새롭게 만들다보니 프로젝트가 복잡해지고, 개발 효율성도 떨어지는 것 같았다.

따라서 여러 종류의 툴팁을 유연하게 관리하고, 빠르게 사용할 수 있도록 프레임워크를 만들기로 했다.


우선, 툴팁은 마우스가 오버되면 보여지고, 아웃되면 숨겨져야 하기 때문에 IPointer 인터페이스로 TooltipTrigger 클래스를 만들어주었다.

public class TooltipTrigger : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    public void OnPointerEnter(PointerEventData eventData)
    {
        // 툴팁 보여주기
        TooltipManager.Instance.Show(this);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        // 툴팁 숨기기
        TooltipManager.Instance.Hide(this);
    }
}

 

툴팁을 TooltipTrigger 클래스에서 관리해도 되지만, 최적화를 위해 최대한 툴팁이 재사용되었으면 좋겠기에 TooltipManager를 만들어 Dictionary로 관리하도록 하였다.

public class TooltipManager : Singleton<TooltipManager>
{
    private Dictionary<GameObject, TooltipStyle> _tooltip = new Dictionary<GameObject, TooltipStyle>();
    private Transform _parent;
    
    internal void Show(TooltipTrigger trigger)
    {
        
    }
    
    internal void Hide(TooltipTrigger trigger)
    {
    
    }
}

 

이후, 툴팁들에 데이터를 넣어주는 방식에 대해 고민하였다.

툴팁들은 적용되야하는 텍스트, 이미지 등의 개수 또한 달랐기에 어떻게 효율적으로 관리할 수 있을지 고민했다.

처음에는 무작정 TooltipStyle을 추상클래스로 만들고, 종류에 따라 TooltipStyle을 상속받은 클래스를 만들어보았다.

public abstract class TooltipStyle : UIBase 
{
    internal abstract void ApplyData(string str);
    internal abstract void ApplyData(Sprite sprite)
}
public class DescriptionTooltipStyle : TooltipStyle
{
    public TextMeshProUGUI descriptionText;
    
    internal override void ApplyData(string str)
    {
        descriptionText.text = str;
    }
    
    internal override void ApplyData(Sprite sprite)
    {
        
    }
}
public class SpriteTooltipStyle : TooltipStyle
{
    public Image image;
    
    internal override void ApplyData(string str)
    {
        
    }
    
    internal override void ApplyData(Sprite sprite)
    {
        image.sprite = sprite;
    }
}

 

하지만 이렇게 구현한다면, 전달해야하는 데이터의 개수나 타입이 달라질 때 마다 자식 스크립트까지 모두 수정해야하였기에 추후 수정이 불편하였고, 인터페이스로 구현한다면 호출하는 부분에서 잦은 수정이 일어날 것이였기에 다른 방법을 찾아야 했다.

 

고심끝에 Dictionary로 관리하여 캐싱해둔 키 값에 맞는 string과 Sprite를 반환하도록 TooltipData를 만들기로 했다.

Dictionary는 직렬화가 되지 않기에 List에 키와 값을 저장해두고, 게임 실행 시 딕셔너리에 옮기도록 구현하였다.

[System.Serializable]
public class TooltipData
{
    [SerializeField] private List<string> _stringKeys = new List<string>();
    [SerializeField] private List<string> _spriteKeys = new List<string>();

    [SerializeField] private List<string> _stringValues = new List<string>();
    [SerializeField] private List<Sprite> _spriteValues = new List<Sprite>();

    private Dictionary<string, string> _stringData = new Dictionary<string, string>();
    private Dictionary<string, Sprite> _spriteData = new Dictionary<string, Sprite>();

    internal Dictionary<string, string> getAllString => _stringData;
    internal Dictionary<string, Sprite> getAllSprite => _spriteData;

    public void InitializeData()
    {
        _stringData = _stringKeys.Zip(_stringValues, (key, value) => new { key, value })
                         .ToDictionary(x => x.key, x => x.value);
        _spriteData = _spriteKeys.Zip(_spriteValues, (key, value) => new { key, value })
                          .ToDictionary(x => x.key, x => x.value);
    }
}

 

이제 툴팁 스타일에서 ApplyData를 할 때, TooltipData를 넘겨주어 값을 설정해줄 수 있었다.

또한, 에디터에서 스타일에 맞는 데이터들만 보여주기 위해 CreateField 추상 메서드도 추가해주었다.

public abstract class TooltipStyle : UIBase
{
#if UNITY_EDITOR
    internal abstract TooltipData CreateField();
#endif
    internal abstract void ApplyData(TooltipData data);
}
public class DescriptionTooltipStyle : TooltipStyle
{
    public TextMeshProUGUI descriptionText;
    
#if UNITY_EDITOR
        internal override TooltipData CreateField()
        {
            var data = new TooltipData();

            data.AddString("Description", "");

            return data;
        }
#endif
    
    internal override async void ApplyData(TooltipData data)
    {
        descriptionText.text = data.GetString("Description");
        
        // 다음 프레임까지 대기 ▶ 텍스트 정보 갱신할 시간을 주기 위해
        await UniTask.Yield();
        
        // 높이 조절
        var newHeight = descriptionText.textInfo.lineCount * 50;
        Vector2 sizeDelta = descriptionText.rectTransform.sizeDelta;
        sizeDelta.y = newHeight;
        descriptionText.rectTransform.sizeDelta = sizeDelta;
    }
}

 

CreateField 메서드에서 TooltipData의 키-값 리스트에 데이터를 추가해 주어야 하기에 TooltipData에 데이터를 추가할 수 있는 메서드를 구현해 주었다.

[System.Serializable]
public class TooltipData
{
#if UNITY_EDITOR
    #region 데이터 추가
    internal void AddString(string key, string value)
    {
        if (_stringData.ContainsKey(key)) return;

        _stringKeys.Add(key);
        _stringValues.Add(value);
        _stringData[key] = value;
    }

    internal void AddSprite(string key, Sprite value)
    {
        if (_spriteData.ContainsKey(key)) return;

        _spriteKeys.Add(key);
        _spriteValues.Add(value);
        _spriteData[key] = value;
    }
    #endregion
#endif
}

 

TooltipTrigger에 툴팁 스타일을 넣으면 데이터를 변경할 수 있도록 변수로 추가해주고, CustomEditor를 사용해서 필요한 데이터만 이쁘게 보이도록 구현해주었다.

public class TooltipTrigger : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    [SerializeField] private TooltipStyle _tooltipStyle;
    [SerializeField] private TipPosition _tooltipPosition;
    [SerializeField] private Vector2 _tooltipOffset;

    [HideInInspector] public TooltipData tooltipData;
    
    internal TooltipStyle tooltipStyle { get => _tooltipStyle; set => _tooltipStyle = value; }
    internal TipPosition tooltipPosition => _tooltipPosition;
    internal Vector2 tooltipOffset => _tooltipOffset;

    private void Awake()
    {
        tooltipData.InitializeData();
    }
    
    public void OnPointerEnter(PointerEventData eventData)
    {
        TooltipManager.Instance.Show(this);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        TooltipManager.Instance.Hide(this);
    }
}

결과