UnityのUI実装でありがちなのが、「とりあえず全部をひとつのスクリプトのUpdateに書いてしまう」パターンですね。マウス入力の取得も、UIテキストの更新も、フェード演出も、全部1ファイルに詰め込んでしまうと、あとから「ツールチップの仕様をちょっと変えたい」と思ったときに、どこを触ればいいのか分からなくなりがちです。

そこでこの記事では、「マウスホバー時に、設定されたテキストを別レイヤーのツールチップUIに表示する」ための専用コンポーネント Tooltip (ツールチップ) を用意して、表示ロジックUI側の描画ロジック をきれいに分離してみましょう。

プレイヤーや敵オブジェクト、インベントリのアイコンなど、どのオブジェクトにも「説明文を出す能力」を小さなコンポーネントとして付け外しできるようにしておくと、レベルデザインやUI調整がかなり楽になります。

【Unity】ホバーするだけで説明表示!「Tooltip」コンポーネント

ここでは以下の2コンポーネント構成で実装します。

  • TooltipUI … 画面上にツールチップを表示する「UI側の受け皿」コンポーネント
  • Tooltip … 任意のオブジェクトに付けて、「どのテキストを、いつ表示するか」を決めるコンポーネント

どのゲームオブジェクトも、Tooltip を付けるだけで共通の TooltipUI にテキストを送って表示してもらう、というイメージですね。

フルコード:TooltipUI と Tooltip


using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

namespace TooltipSample
{
    /// <summary>
    /// 画面上にツールチップを表示するためのUIコンポーネント。
    /// キャンバス上の専用レイヤー(Tooltipレイヤー)に配置して使います。
    /// </summary>
    public class TooltipUI : MonoBehaviour
    {
        // シングルトン的にどこからでもアクセスできるようにする
        public static TooltipUI Instance { get; private set; }

        [Header("参照")]
        [SerializeField] private CanvasGroup canvasGroup;   // フェード制御用
        [SerializeField] private Text tooltipText;          // 表示テキスト
        [SerializeField] private RectTransform background;  // 背景パネル
        [SerializeField] private RectTransform canvasRect;  // 親キャンバスのRectTransform

        [Header("設定")]
        [SerializeField] private Vector2 padding = new Vector2(16f, 8f); // テキスト周りの余白
        [SerializeField] private Vector2 offset = new Vector2(16f, -16f); // マウス位置からのオフセット
        [SerializeField] private bool followMouse = true;               // マウスに追従するかどうか

        private bool isShowing;
        private Camera uiCamera;

        private void Awake()
        {
            // シングルトンのセットアップ
            if (Instance != null && Instance != this)
            {
                Debug.LogWarning("複数のTooltipUIがシーン上に存在します。最初のものだけが使用されます。");
                return;
            }
            Instance = this;

            // CanvasGroupが未設定なら自動取得を試みる
            if (canvasGroup == null)
            {
                canvasGroup = GetComponentInChildren<CanvasGroup>();
            }

            // Tooltipはデフォルトで非表示にしておく
            HideImmediate();

            // UI用カメラを取得(Screen Space - Overlay の場合は null でOK)
            var canvas = canvasRect != null ? canvasRect.GetComponentInParent<Canvas>() : GetComponentInParent<Canvas>();
            if (canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay)
            {
                uiCamera = canvas.worldCamera;
            }
        }

        private void Update()
        {
            // 表示中で、かつマウス追従が有効なら位置を更新
            if (isShowing && followMouse)
            {
                UpdatePositionToMouse();
            }
        }

        /// <summary>
        /// ツールチップを表示する(テキストをセットしてフェードイン)。
        /// </summary>
        /// <param name="message">表示するテキスト</param>
        public void Show(string message)
        {
            if (tooltipText == null || background == null)
            {
                Debug.LogWarning("TooltipUIの参照が設定されていません。");
                return;
            }

            isShowing = true;

            // テキストをセット
            tooltipText.text = message;

            // テキストのサイズに合わせて背景サイズを調整
            LayoutRebuilder.ForceRebuildLayoutImmediate(tooltipText.rectTransform);
            Vector2 textSize = tooltipText.rectTransform.sizeDelta;
            background.sizeDelta = textSize + padding;

            // 位置を更新
            UpdatePositionToMouse();

            // 表示
            canvasGroup.alpha = 1f;
            canvasGroup.blocksRaycasts = false; // Tooltip自身でクリックをブロックしない
            canvasGroup.interactable = false;
        }

        /// <summary>
        /// ツールチップを非表示にする。
        /// </summary>
        public void Hide()
        {
            isShowing = false;
            canvasGroup.alpha = 0f;
        }

        /// <summary>
        /// 即座に非表示(初期化用)。
        /// </summary>
        private void HideImmediate()
        {
            isShowing = false;
            if (canvasGroup != null)
            {
                canvasGroup.alpha = 0f;
            }
        }

        /// <summary>
        /// マウス位置に追従してツールチップの位置を更新する。
        /// </summary>
        private void UpdatePositionToMouse()
        {
            if (canvasRect == null) return;

            // 現在のマウス位置(スクリーン座標)
            Vector2 mousePos = Input.mousePosition;

            // スクリーン座標からキャンバス座標へ変換
            RectTransformUtility.ScreenPointToLocalPointInRectangle(
                canvasRect,
                mousePos,
                uiCamera,
                out Vector2 localPoint
            );

            // オフセットを加算
            localPoint += offset;

            // キャンバス内に収まるようにクランプ
            Vector2 halfSize = canvasRect.rect.size * 0.5f;
            Vector2 bgHalfSize = background.rect.size * 0.5f;

            float clampedX = Mathf.Clamp(localPoint.x, -halfSize.x + bgHalfSize.x, halfSize.x - bgHalfSize.x);
            float clampedY = Mathf.Clamp(localPoint.y, -halfSize.y + bgHalfSize.y, halfSize.y - bgHalfSize.y);

            background.anchoredPosition = new Vector2(clampedX, clampedY);
        }
    }

    /// <summary>
    /// 任意のゲームオブジェクトに付けて、
    /// マウスホバー時にTooltipUIへテキストを送るコンポーネント。
    /// </summary>
    [DisallowMultipleComponent]
    public class Tooltip : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
    {
        [Header("ツールチップ設定")]
        [TextArea(2, 5)]
        [SerializeField] private string tooltipText = "ここに説明テキストを設定します。";

        [SerializeField] private float showDelay = 0.2f; // ホバーしてから表示するまでの遅延秒数

        private bool isPointerOver;
        private float hoverTimer;

        private void Update()
        {
            // ポインタが乗っている間だけタイマーを進める
            if (isPointerOver)
            {
                hoverTimer += Time.unscaledDeltaTime; // ポーズ中でも動くようにunscaled

                // 遅延時間を超えたら表示
                if (hoverTimer >= showDelay)
                {
                    ShowTooltip();
                }
            }
        }

        /// <summary>
        /// ポインタがオブジェクトに入った時に呼ばれる(EventSystem経由)。
        /// </summary>
        public void OnPointerEnter(PointerEventData eventData)
        {
            isPointerOver = true;
            hoverTimer = 0f;
        }

        /// <summary>
        /// ポインタがオブジェクトから出た時に呼ばれる(EventSystem経由)。
        /// </summary>
        public void OnPointerExit(PointerEventData eventData)
        {
            isPointerOver = false;
            hoverTimer = 0f;
            HideTooltip();
        }

        /// <summary>
        /// TooltipUIにテキストを送って表示させる。
        /// </summary>
        private void ShowTooltip()
        {
            if (TooltipUI.Instance == null) return;

            TooltipUI.Instance.Show(tooltipText);
        }

        /// <summary>
        /// TooltipUIに非表示を依頼する。
        /// </summary>
        private void HideTooltip()
        {
            if (TooltipUI.Instance == null) return;

            TooltipUI.Instance.Hide();
        }

        /// <summary>
        /// ゲーム中にツールチップのテキストを差し替えたい場合に使えるヘルパー。
        /// </summary>
        /// <param name="text">新しいテキスト</param>
        public void SetTooltipText(string text)
        {
            tooltipText = text;

            // すでにホバー中で、表示済みなら即時更新してもよい
            if (isPointerOver && TooltipUI.Instance != null)
            {
                TooltipUI.Instance.Show(tooltipText);
            }
        }
    }
}

使い方の手順

ここでは、インベントリのアイコン にツールチップを付ける例で説明します。他にも、プレイヤーのスキルボタンや敵アイコンなど、同じ手順で使えます。

  1. ① Tooltip用のUIレイヤーを作る
    • Hierarchy で Canvas を用意(既にある場合はそれを利用)。
    • Canvas の子に Panel を作成し、名前を TooltipLayer などに変更。
    • Panel の中に Image(背景)と Text(説明テキスト)を配置。
    • 背景ImageのRectTransformを左上アンカーにしておくと扱いやすいです。
    • Panel には CanvasGroup コンポーネントを追加しておきます。
  2. ② TooltipUI コンポーネントを設定する
    • 空のGameObjectを作成し、名前を TooltipUI にします。
    • このGameObjectに上記コードの TooltipUI コンポーネントをアタッチ。
    • インスペクタで以下を割り当てます:
      • Canvas Group:①で作成した Panel 上の CanvasGroup
      • Tooltip Text:Panel 内の Text
      • Background:Panel 自体の RectTransform
      • Canvas Rect:親Canvasの RectTransform
    • TooltipLayer(背景パネル)はデフォルトで画面外にあってもOKですが、CanvasGroupのAlphaを0にしておくと安心です。
  3. ③ Tooltip を付けたいUI要素にコンポーネントを追加
    例:インベントリのアイコンボタンにツールチップを付けたい場合
    • インベントリのアイコン(ButtonImage)のGameObjectを選択。
    • Tooltip コンポーネントをアタッチ。
    • インスペクタの Tooltip Text に、表示したい説明文を入力します。

      例:
      • 「HPを50回復するポーションです。」
      • 「攻撃力+10の剣。クリティカル率+5%。」
    • Show Delay を 0.2〜0.5秒程度にすると、誤ホバーでの点滅を防ぎやすいです。
    • EventSystem(EventSystem コンポーネントを持つオブジェクト)がシーンに存在することを確認してください(Buttonなどを作ると自動で追加されています)。
  4. ④ 実行して確認する
    • Playボタンを押してゲームを実行。
    • Tooltipコンポーネントを付けたアイコンにマウスカーソルを乗せると、少し遅れてTooltipLayer上にテキストが表示されます。
    • マウスを外すと、Tooltipは非表示になります。

同じ手順で、スキルボタン敵アイコンミニマップ上のマーカー などにも Tooltip コンポーネントを付けるだけで、共通のTooltipUIに説明文を表示できます。

メリットと応用

この構成のいいところは、「表示の見た目」と「どのテキストをいつ出すか」を完全に分離できている点です。

  • レイアウト変更が楽:TooltipUI側の背景画像やフォント、アニメーションを変えても、各Tooltipコンポーネントには一切手を入れる必要がありません。
  • プレハブ管理がシンプル:アイテムやスキルのプレハブに Tooltip コンポーネントを付けておけば、どのシーンに置いても同じ挙動でツールチップが出ます。
  • 責務が小さい:Tooltip は「ホバー検知とテキスト送信」だけ、TooltipUI は「表示位置と見た目の制御」だけと役割が明確なので、バグの切り分けがしやすいです。
  • 仕様変更に強い:「ツールチップはマウス追従じゃなくて、画面下に固定で出したい」などの要望が出ても、TooltipUI側だけを書き換えれば済みます。

例えば、インベントリシステムを作るときは、「アイテムのデータ(ScriptableObject)」「UIボタン」 の間に Tooltip を挟んでおくと、アイテムデータ側の説明文をTooltip.Textに流し込むだけで、どのシーンでも同じ表現が使い回せます。

応用として、ツールチップの見た目にちょっとしたアニメーションを付けたければ、TooltipUIにフェード処理を追加するだけでOKです。例えば以下のように、簡単なフェードイン を追加できます。


private Coroutine fadeCoroutine;

public void ShowWithFade(string message, float fadeDuration = 0.15f)
{
    Show(message); // まずは即時で内容と位置をセット

    if (fadeCoroutine != null)
    {
        StopCoroutine(fadeCoroutine);
    }
    fadeCoroutine = StartCoroutine(FadeInCoroutine(fadeDuration));
}

private System.Collections.IEnumerator FadeInCoroutine(float duration)
{
    canvasGroup.alpha = 0f;
    float t = 0f;
    while (t < duration)
    {
        t += Time.unscaledDeltaTime;
        float normalized = Mathf.Clamp01(t / duration);
        canvasGroup.alpha = normalized;
        yield return null;
    }
    canvasGroup.alpha = 1f;
}

このように、TooltipUI側にだけ演出コードを足していく ことで、ゲーム全体のツールチップ表現を一括でリッチにしていけます。Tooltipコンポーネントはあくまで小さく、責務を増やさないのがポイントですね。