【Godot 4】TooltipTrigger (ツールチップ) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Godotでアイテムのツールチップを作ろうとすると、つい「UI用のベースシーンを継承して…」「InventoryItemButton を継承して…」みたいな感じで、どんどん継承ツリーが伸びていきがちですよね。さらに、mouse_entered / mouse_exited をそれぞれのボタンに書いて、表示用の LabelPopupPanel をシーンごとに用意して…となると、管理がかなり面倒です。

そこで今回は、「どんなノードにも後付けでツールチップ機能を生やせる」コンポーネント TooltipTrigger を作ってみましょう。
アイテムアイコン、スキルボタン、NPCの頭上アイコンなどにポン付けするだけで、同じ仕組みのツールチップがサクッと使えるようになります。

【Godot 4】乗せるだけでツールチップ!「TooltipTrigger」コンポーネント

今回のコンポーネントの思想はシンプルです。

  • アイテムアイコン側は「どんな情報を表示するか」だけを持つ
  • 実際の表示(位置、見た目、アニメーション)は「ツールチップUIシーン」に任せる
  • TooltipTrigger は「マウスイベントを拾って、ツールチップUIに通知するだけ」の薄いコンポーネント

つまり、見た目とロジックを分離しつつ、継承ではなくコンポーネントとして後から追加できる構成ですね。


TooltipTrigger.gd(フルコード)


extends Node
class_name TooltipTrigger
## マウスホバー時にツールチップを表示するコンポーネント
##
## - 任意の Control / Node2D などにアタッチして使う
## - 実際の表示は TooltipUI シーンに任せる
## - このコンポーネントは「いつ・何を表示するか」だけを管理する

@export_category("Tooltip Content")
## ツールチップのタイトル(アイテム名など)
@export var title: String = ""

## メインの説明文(複数行OK)
@export_multiline var description: String = ""

## 追加情報(レアリティ、ショートカットキーなど)
@export_multiline var extra_info: String = ""

@export_category("Tooltip Behaviour")
## ツールチップを表示するまでの遅延秒数
@export var show_delay_sec: float = 0.3

## ツールチップを閉じるまでの遅延秒数(ホバー解除時)
@export var hide_delay_sec: float = 0.1

## このツールチップが有効かどうか(無効なら何もしない)
@export var enabled: bool = true

## ワールド座標ではなく、親Controlのローカル座標で表示したい場合にON
## 例: インベントリUI内で、同じCanvasLayer上にツールチップを出したいとき
@export var use_local_position: bool = true

@export_category("Tooltip UI Reference")
## 実際に表示を担当するツールチップUIノードへの参照
## 例: シーンのどこかに置いた TooltipUI (Control) をドラッグ&ドロップで指定
@export var tooltip_ui: NodePath

## マウスが乗っている間 true
var _hovering: bool = false
## 遅延表示用タイマー
var _show_timer: SceneTreeTimer
## 遅延非表示用タイマー
var _hide_timer: SceneTreeTimer

func _ready() -> void:
    # 入力を受け取れるようにする(Control 以外にも対応するため)
    _setup_mouse_signals()
    # 親シーンから TooltipUI を探すフォールバック
    if tooltip_ui.is_empty():
        var found := _find_tooltip_ui_in_tree()
        if found:
            tooltip_ui = found.get_path()

func _setup_mouse_signals() -> void:
    # Control 系なら mouse_entered / mouse_exited シグナルを使う
    if owner is Control:
        owner.mouse_entered.connect(_on_mouse_entered)
        owner.mouse_exited.connect(_on_mouse_exited)
    else:
        # Node2D などの場合は、InputEvent を拾って判定する
        set_process_input(true)

func _input(event: InputEvent) -> void:
    if not enabled:
        return
    if not is_instance_valid(owner):
        return

    # Control 以外にアタッチしている場合: 簡易的なホバー判定を行う
    if owner is Node2D and event is InputEventMouseMotion:
        var node2d := owner as Node2D
        var viewport := node2d.get_viewport()
        if viewport == null:
            return

        var mouse_pos := event.position
        # 当たり判定は CollisionShape2D や Sprite2D の Rect などに合わせて各自調整
        # ここでは簡易的に「owner の原点付近 ±32px 四方」で判定する例
        var rect := Rect2(node2d.global_position - Vector2(32, 32), Vector2(64, 64))
        var inside := rect.has_point(mouse_pos)

        if inside and not _hovering:
            _on_mouse_entered()
        elif not inside and _hovering:
            _on_mouse_exited()

func _on_mouse_entered() -> void:
    if not enabled:
        return
    _hovering = true
    # 非表示タイマーが動いていたらキャンセル
    if _hide_timer and _hide_timer.is_valid():
        _hide_timer = null

    # 遅延表示タイマーをセット
    if show_delay_sec <= 0.0:
        _show_tooltip()
    else:
        _show_timer = get_tree().create_timer(show_delay_sec)
        _show_timer.timeout.connect(_show_tooltip, CONNECT_ONE_SHOT)

func _on_mouse_exited() -> void:
    _hovering = false
    # 表示タイマーをキャンセル
    if _show_timer and _show_timer.is_valid():
        _show_timer = null

    # 遅延非表示タイマーをセット
    if hide_delay_sec <= 0.0:
        _hide_tooltip()
    else:
        _hide_timer = get_tree().create_timer(hide_delay_sec)
        _hide_timer.timeout.connect(_hide_tooltip, CONNECT_ONE_SHOT)

func _show_tooltip() -> void:
    if not _hovering:
        return
    var ui := _get_tooltip_ui()
    if ui == null:
        push_warning("TooltipTrigger: Tooltip UI not found. Please assign 'tooltip_ui' or place a TooltipUI node in the scene.")
        return

    # 表示位置を計算
    var pos := _get_tooltip_position()
    # TooltipUI にデータを渡して表示してもらう
    if ui.has_method("show_tooltip"):
        ui.show_tooltip(title, description, extra_info, pos)
    else:
        push_warning("TooltipTrigger: Tooltip UI does not implement 'show_tooltip(title, description, extra_info, position)'")

func _hide_tooltip() -> void:
    var ui := _get_tooltip_ui()
    if ui and ui.has_method("hide_tooltip"):
        ui.hide_tooltip()

func _get_tooltip_ui() -> Node:
    if tooltip_ui.is_empty():
        return null
    if not is_inside_tree():
        return null
    var node := get_node_or_null(tooltip_ui)
    return node

func _find_tooltip_ui_in_tree() -> Node:
    # シーンツリーの上方向に向かって TooltipUI らしきノードを探す簡易実装
    var current := get_parent()
    while current:
        if current.has_method("show_tooltip") and current.has_method("hide_tooltip"):
            return current
        current = current.get_parent()
    return null

func _get_tooltip_position() -> Vector2:
    # owner が Control なら、そのローカル座標 or グローバル座標から位置を算出
    if owner is Control:
        var ctrl := owner as Control
        var base_pos := ctrl.get_global_rect().end  # 右下あたり
        if use_local_position and _get_tooltip_ui() is Control:
            var ui_ctrl := _get_tooltip_ui() as Control
            # グローバル座標 → UIローカル座標に変換
            base_pos = ui_ctrl.get_global_transform().affine_inverse().xform(base_pos)
        return base_pos + Vector2(8, 8) # 少しオフセット

    # Node2D の場合はグローバル座標をそのまま使う
    if owner is Node2D:
        var node2d := owner as Node2D
        return node2d.global_position + Vector2(16, -16)

    # それ以外はとりあえずスクリーン中央
    var viewport := get_viewport()
    if viewport:
        return viewport.get_visible_rect().size / 2.0
    return Vector2.ZERO

## 外部から内容を書き換えるためのヘルパー
func set_tooltip_content(p_title: String, p_description: String, p_extra_info: String = "") -> void:
    title = p_title
    description = p_description
    extra_info = p_extra_info

TooltipUI.gd(参考実装:表示担当のUIコンポーネント)

TooltipTrigger は「ツールチップUIに任せる」設計なので、最低限以下のメソッドを持つ UI ノードを用意する必要があります。

  • show_tooltip(title: String, description: String, extra_info: String, position: Vector2)
  • hide_tooltip()

以下は、CanvasLayer 配下に置くシンプルなツールチップUIの例です。


extends Control
class_name TooltipUI
## TooltipTrigger から呼び出される表示専用UI
##
## このノードをシーンのどこか(例: CanvasLayer 内)に1つ置き、
## TooltipTrigger の export から参照させます。

@onready var title_label: Label = %TitleLabel
@onready var description_label: Label = %DescriptionLabel
@onready var extra_label: Label = %ExtraLabel
@onready var panel: Panel = %Panel

func _ready() -> void:
    hide()

func show_tooltip(title: String, description: String, extra_info: String, position: Vector2) -> void:
    title_label.text = title
    description_label.text = description
    extra_label.text = extra_info

    # 空なら非表示にしてもOK
    extra_label.visible = extra_info.strip_edges() != ""

    panel.global_position = position
    show()
    panel.show()

func hide_tooltip() -> void:
    hide()

シーン構成例:

CanvasLayer
 └── TooltipUI (Control)
      └── Panel (%Panel)
           ├── TitleLabel (Label)      [%TitleLabel]
           ├── DescriptionLabel (Label)[%DescriptionLabel]
           └── ExtraLabel (Label)      [%ExtraLabel]

使い方の手順

  1. TooltipUI シーンを作る
    上記の TooltipUI.gd を Control ノードにアタッチし、Panel + Label 3つを子に持つ構成で作成します。
    完成したら、ゲームのルートシーン(例: Main)に常駐させておきましょう。
  2. アイテムアイコンシーンに TooltipTrigger を追加する
    例えば、インベントリ用のアイテムボタンシーンを以下のように組みます。
        ItemIcon (TextureButton)
         ├── TextureRect (アイコン画像)
         └── TooltipTrigger (Node)
        
    • TooltipTrigger.gdTooltipTrigger ノードにアタッチ
    • インスペクタで title, description, extra_info を設定
    • tooltip_ui に、シーン上の TooltipUI ノードをドラッグ&ドロップ

    これで、マウスをアイテムアイコンに乗せるとツールチップがポップアップするようになります。

  3. プレイヤーや敵にも再利用する
    例えば、フィールド上のアイテムや敵にも、同じコンポーネントをそのまま付けられます。
        WorldItem (Node2D)
         ├── Sprite2D
         ├── CollisionShape2D
         └── TooltipTrigger (Node)
        
    • タイトル: 「回復ポーション」
    • 説明: 「HPを50回復する。」
    • 追加情報: 「右クリックで使用」など

    こうしておけば、UIだろうがワールド上だろうが、「TooltipTrigger を付ける」だけで統一されたツールチップが出せます。

  4. スキルボタンなど別UIにも展開する
    スキルバーのボタンにもまったく同じコンポーネントを付けるだけです。
        SkillButton (Button)
         ├── TextureRect (スキルアイコン)
         └── TooltipTrigger (Node)
        

    これで、「アイテム」「スキル」「NPCアイコン」など、異なるシーンでも同じツールチップシステムを使い回せます。
    継承で ItemButton, SkillButton などを増やさなくていいので、シーン構造がかなりスッキリしますね。


メリットと応用

この TooltipTrigger コンポーネントを使う一番のメリットは、「ツールチップが欲しくなったらノードを1個足すだけ」という点です。

  • 継承ベースで「ツールチップ付きボタン」「ツールチップ付きアイテム」などを乱立させなくて済む
  • ツールチップの見た目変更は TooltipUI シーンをいじるだけで全体に反映される
  • ゲームロジック(アイテム効果、スキル処理など)とは完全に分離されているので、保守が楽
  • インベントリUI、スキルバー、ショップ画面、ワールド上のオブジェクト…どこでも同じ仕組みを再利用可能

特に、Godot でありがちな「UIシーンのノード階層がどんどん深くなっていく」問題をかなり抑えられます。
各アイテムアイコンは「見た目 + TooltipTrigger」だけを持ち、表示ロジックは中央集権的な TooltipUI に任せる構成なので、コンポーネント志向らしくスリムなツリーにできます。

改造案:キーボードフォーカスでもツールチップを表示する

ゲームパッド操作やキーボード操作でインベントリを操作する場合、「カーソルホバー」だけではなく「フォーカスが当たったとき」にもツールチップが出てほしいですよね。
そんなときは、TooltipTrigger に以下のようなメソッドを足して、フォーカスイベントから呼ぶようにしてもOKです。


## フォーカス獲得時にツールチップを強制表示するためのAPI
func show_on_focus() -> void:
    if not enabled:
        return
    _hovering = true
    _show_tooltip()

## フォーカス喪失時にツールチップを閉じるためのAPI
func hide_on_unfocus() -> void:
    _hovering = false
    _hide_tooltip()

あとは、ボタン側で:


func _on_focus_entered() -> void:
    %TooltipTrigger.show_on_focus()

func _on_focus_exited() -> void:
    %TooltipTrigger.hide_on_unfocus()

という感じで呼べば、マウスでもキーボードでも同じツールチップシステムを使い回せます。
コンポーネントを1つ追加するだけで UI 体験が一気に良くなるので、ぜひプロジェクトに組み込んでみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!