Godotでアイテムのツールチップを作ろうとすると、つい「UI用のベースシーンを継承して…」「InventoryItemButton を継承して…」みたいな感じで、どんどん継承ツリーが伸びていきがちですよね。さらに、mouse_entered / mouse_exited をそれぞれのボタンに書いて、表示用の Label や PopupPanel をシーンごとに用意して…となると、管理がかなり面倒です。
そこで今回は、「どんなノードにも後付けでツールチップ機能を生やせる」コンポーネント 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]
使い方の手順
-
TooltipUI シーンを作る
上記のTooltipUI.gdを Control ノードにアタッチし、Panel + Label 3つを子に持つ構成で作成します。
完成したら、ゲームのルートシーン(例:Main)に常駐させておきましょう。 -
アイテムアイコンシーンに TooltipTrigger を追加する
例えば、インベントリ用のアイテムボタンシーンを以下のように組みます。ItemIcon (TextureButton) ├── TextureRect (アイコン画像) └── TooltipTrigger (Node)TooltipTrigger.gdをTooltipTriggerノードにアタッチ- インスペクタで
title,description,extra_infoを設定 tooltip_uiに、シーン上のTooltipUIノードをドラッグ&ドロップ
これで、マウスをアイテムアイコンに乗せるとツールチップがポップアップするようになります。
-
プレイヤーや敵にも再利用する
例えば、フィールド上のアイテムや敵にも、同じコンポーネントをそのまま付けられます。WorldItem (Node2D) ├── Sprite2D ├── CollisionShape2D └── TooltipTrigger (Node)- タイトル: 「回復ポーション」
- 説明: 「HPを50回復する。」
- 追加情報: 「右クリックで使用」など
こうしておけば、UIだろうがワールド上だろうが、「TooltipTrigger を付ける」だけで統一されたツールチップが出せます。
-
スキルボタンなど別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 体験が一気に良くなるので、ぜひプロジェクトに組み込んでみてください。




