Godot でボタンやアイコンをちょっとだけリッチに見せたいとき、つい Button を継承したカスタムクラスを量産したり、各シーンのスクリプトに「マウスが乗ったら拡大する」処理をコピペしがちですよね。
でもそれをやり続けると、
- UIごとに似たようなスクリプトが増える
- ちょっと挙動を変えたいだけなのに、全部のボタンを修正する羽目になる
- 継承ツリーが増えて、どこで何をやっているのか分かりづらくなる
Godot 標準の深いノード階層+継承ベースで作っていると、UI演出のような「小さいけど数が多い」機能は特にメンテしづらくなります。
そこで今回は、ボタンやアイコンに「後付けできる」ホバー演出コンポーネント HoverScale を用意してみましょう。
このコンポーネントを任意のノードの子としてアタッチするだけで、
- マウスホバー時に親ノードをふわっと拡大
- ホバー解除時に元のサイズにスムーズに戻す
- ボタンでもアイコンでも、
ControlでもNode2Dでも使える
という「合成で付け替え可能な UI 演出」を実現します。
【Godot 4】ホバーでふわっと拡大!「HoverScale」コンポーネント
フルコード(GDScript / Godot 4)
extends Node
class_name HoverScale
## 親ノードにマウスが乗ったとき、少しだけ拡大するコンポーネント。
##
## - 2D系(Node2D, Control)どちらでも利用可能
## - Tweenでふわっと拡大・縮小
## - マウスイベントを親から受け取って動作
@export_range(0.0, 3.0, 0.01)
var hover_scale: float = 1.1:
set(value):
hover_scale = max(value, 0.0)
## 拡大・縮小にかける時間(秒)
@export_range(0.0, 1.0, 0.01)
var duration: float = 0.08
## Tween のイージング
@export var transition_type: Tween.TransitionType = Tween.TRANS_QUAD
@export var ease_type: Tween.EaseType = Tween.EASE_OUT
## マウスがホバーしている間、クリック入力をブロックするかどうか
## (UIレイヤーによっては不要なのでオフにできるように)
@export var block_mouse_input: bool = false
## 内部状態
var _original_scale: Vector2 = Vector2.ONE
var _is_hovered: bool = false
var _tween: Tween
func _ready() -> void:
# 親が存在しないと何もできないのでチェック
if not get_parent():
push_warning("HoverScale: 親ノードがありません。何も拡大できません。")
return
# 親のタイプによってスケールの取り扱いが違うので分岐
if get_parent() is Node2D:
_original_scale = (get_parent() as Node2D).scale
elif get_parent() is Control:
# Control は scale プロパティを持たないので
# rect_scale を使う(Godot 4 では scale と同等)
_original_scale = (get_parent() as Control).scale
else:
push_warning("HoverScale: 親ノードが Node2D でも Control でもありません。スケールを変更できません。")
return
# 親のマウスイベントを拾うために、親にマウス入力を有効化しておく
# Control 系なら mouse_filter を変更、Node2D 系なら input_pickable を有効化
if get_parent() is Control:
var c := get_parent() as Control
if c.mouse_filter == Control.MOUSE_FILTER_IGNORE:
c.mouse_filter = Control.MOUSE_FILTER_STOP
elif get_parent() is Node2D:
var n := get_parent() as Node2D
n.input_pickable = true
# 親のマウスイベントシグナルに接続
_connect_parent_signals()
func _connect_parent_signals() -> void:
var p := get_parent()
if not p:
return
# Control 系のホバー検知
if p is Control:
var c := p as Control
if not c.is_connected("mouse_entered", Callable(self, "_on_parent_mouse_entered")):
c.mouse_entered.connect(_on_parent_mouse_entered)
if not c.is_connected("mouse_exited", Callable(self, "_on_parent_mouse_exited")):
c.mouse_exited.connect(_on_parent_mouse_exited)
# Node2D / その他でも、Viewport のマウスイベントから判定する方法もあるが
# ここでは Control / Node2D 両対応の簡易版として、
# Node2D の場合は input_event シグナルで hover を擬似的に判定する
if p is Node2D and not (p is Control):
var n := p as Node2D
if not n.is_connected("mouse_entered", Callable(self, "_on_parent_mouse_entered")):
n.mouse_entered.connect(_on_parent_mouse_entered)
if not n.is_connected("mouse_exited", Callable(self, "_on_parent_mouse_exited")):
n.mouse_exited.connect(_on_parent_mouse_exited)
func _on_parent_mouse_entered() -> void:
_is_hovered = true
_animate_scale(hover_scale)
func _on_parent_mouse_exited() -> void:
_is_hovered = false
_animate_scale(1.0)
func _animate_scale(target_multiplier: float) -> void:
var p := get_parent()
if not p:
return
# 既に動いている Tween があれば kill
if _tween and _tween.is_valid():
_tween.kill()
_tween = create_tween()
_tween.set_trans(transition_type)
_tween.set_ease(ease_type)
var target_scale := _original_scale * target_multiplier
if p is Node2D:
var n := p as Node2D
_tween.tween_property(n, "scale", target_scale, duration)
elif p is Control:
var c := p as Control
_tween.tween_property(c, "scale", target_scale, duration)
func _unhandled_input(event: InputEvent) -> void:
# block_mouse_input が true の場合、ホバー中はクリック入力をここで消費してしまう例
if not block_mouse_input:
return
if not _is_hovered:
return
if event is InputEventMouseButton and event.pressed:
# クリックをここで止めたい場合(例: ホバー中はクリックさせないなど)
get_viewport().set_input_as_handled()
## --- 改造しやすいように補助関数も用意しておく ---
func reset_scale_immediately() -> void:
## 外部から呼び出して、即座に元のスケールに戻す
var p := get_parent()
if not p:
return
if _tween and _tween.is_valid():
_tween.kill()
if p is Node2D:
(p as Node2D).scale = _original_scale
elif p is Control:
(p as Control).scale = _original_scale
使い方の手順
ここでは代表的な 3 パターンを例にします。
- プレイヤー用 UI ボタン(
Button) - アイコン画像(
TextureRect) - 2D ゲーム内の「動く床」などの
Node2Dオブジェクト
手順①: スクリプトをプロジェクトに追加
- 上記の GDScript を
res://components/hover_scale.gdなど好きな場所に保存します。 - Godot エディタで開くと、クラス名 HoverScale が Inspector の「ノードを追加」ダイアログに出てくるようになります。
手順②: ボタンに HoverScale をアタッチ
例:メインメニューの「Play」ボタンにホバー拡大を付けたい場合。
MainMenu (Control)
├── PlayButton (Button)
│ └── HoverScale (Node)
└── QuitButton (Button)
└── HoverScale (Node)
PlayButtonを選択し、右クリック → 「子ノードを追加」。- 検索欄に「HoverScale」と入力し、HoverScale を追加。
HoverScaleノードを選択して、Inspector から以下を調整:hover_scale:1.1~1.2 くらいがオススメduration:0.05~0.1 くらいでサクッとした感じにtransition_type / ease_type:デフォルト(Quad Out)でOK
これだけで、PlayButton にマウスを乗せるとふわっと拡大するようになります。
同じように QuitButton にも HoverScale を付ければ、コードを書き足さなくても同じ演出を再利用できます。
手順③: アイコン(TextureRect)に適用
例えば、インベントリ UI のアイコンをホバーで強調したい場合:
InventoryItem (Control) ├── Icon (TextureRect) │ └── HoverScale (Node) └── Label (Label)
Icon (TextureRect)の子として HoverScale を追加。hover_scale = 1.15くらいにして、少しだけ目立たせる。- クリックは普通にアイコン側で処理してOK(HoverScale はスケール変更だけ担当)。
継承したカスタム TextureRect を作らなくても、「HoverScale を付けたらホバー演出あり、付けなければ無し」 というシンプルな構成で管理できます。
手順④: 2D ゲーム内オブジェクト(Node2D)に適用
ゲーム内の「動く床」や「拾えるアイテム」に、マウスオーバーでちょっとだけ大きくなる演出を付けたい場合:
MovingPlatform (Node2D) ├── Sprite2D ├── CollisionShape2D └── HoverScale (Node) PickupItem (Node2D) ├── Sprite2D ├── Area2D │ └── CollisionShape2D └── HoverScale (Node)
MovingPlatformやPickupItemの子に HoverScale を追加。- マウスが乗るとオブジェクト全体が少し拡大し、ユーザーに「ここはインタラクト可能だよ」と伝えられます。
このとき、Node2D 側では input_pickable = true が自動で有効になるので、特別な設定は不要です。
メリットと応用
HoverScale コンポーネントを使うことで、UI やオブジェクトのホバー演出を「継承ではなく合成」で管理できるようになります。
- シーン構造がスッキリする
ボタンやアイコンごとにカスタムクラスを作らず、「HoverScale を付けるかどうか」だけで挙動を切り替えられます。 - 演出の一括調整が簡単
HoverScale のスクリプトを 1 箇所変更するだけで、全 UI のホバー感を一括でチューニングできます。 - 既存シーンに後付けしやすい
既にあるシーン構造を壊さず、子ノードとして HoverScale を足すだけで導入可能です。 - 他のコンポーネントと組み合わせやすい
例えば「HoverSound」「HoverColorTint」など、別コンポーネントを追加しても互いに独立して動作します。
「ホバーで拡大するボタン」というカスタムクラスを作るのではなく、
「普通のボタン」+「HoverScale コンポーネント」 という分解された構成にすることで、シーンの見通しと再利用性が一気に上がります。
簡単な改造案:ホバー中だけさらに少し明るくする
例えば、ホバー中は親の modulate を少し明るくしたい場合、以下のような関数を追加できます。
func _set_hover_tint(enabled: bool) -> void:
var p := get_parent()
if not p:
return
var tint := Color.WHITE
if enabled:
# ほんの少しだけ明るく
tint = Color(1.1, 1.1, 1.1, 1.0)
if p is CanvasItem:
(p as CanvasItem).modulate = tint
そして、_on_parent_mouse_entered() / _on_parent_mouse_exited() の中で呼び出します。
func _on_parent_mouse_entered() -> void:
_is_hovered = true
_animate_scale(hover_scale)
_set_hover_tint(true)
func _on_parent_mouse_exited() -> void:
_is_hovered = false
_animate_scale(1.0)
_set_hover_tint(false)
このように、HoverScale をベースに「HoverTint」「HoverSound」などを足していけば、
UI 演出をすべてコンポーネントの組み合わせで表現できるようになります。継承ツリーに悩まされる前に、ぜひ合成ベースの設計を試してみてください。
