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);
}
}
}
}
使い方の手順
ここでは、インベントリのアイコン にツールチップを付ける例で説明します。他にも、プレイヤーのスキルボタンや敵アイコンなど、同じ手順で使えます。
-
① Tooltip用のUIレイヤーを作る
- Hierarchy で
Canvasを用意(既にある場合はそれを利用)。 - Canvas の子に
Panelを作成し、名前をTooltipLayerなどに変更。 - Panel の中に
Image(背景)とText(説明テキスト)を配置。 - 背景ImageのRectTransformを左上アンカーにしておくと扱いやすいです。
- Panel には
CanvasGroupコンポーネントを追加しておきます。
- Hierarchy で
-
② 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にしておくと安心です。
- 空のGameObjectを作成し、名前を
-
③ Tooltip を付けたいUI要素にコンポーネントを追加
例:インベントリのアイコンボタンにツールチップを付けたい場合- インベントリのアイコン(
ButtonやImage)のGameObjectを選択。 Tooltipコンポーネントをアタッチ。- インスペクタの Tooltip Text に、表示したい説明文を入力します。
例:- 「HPを50回復するポーションです。」
- 「攻撃力+10の剣。クリティカル率+5%。」
- Show Delay を 0.2〜0.5秒程度にすると、誤ホバーでの点滅を防ぎやすいです。
- EventSystem(
EventSystemコンポーネントを持つオブジェクト)がシーンに存在することを確認してください(Buttonなどを作ると自動で追加されています)。
- インベントリのアイコン(
-
④ 実行して確認する
- 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コンポーネントはあくまで小さく、責務を増やさないのがポイントですね。
