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 パターンを例にします。

  1. プレイヤー用 UI ボタン(Button
  2. アイコン画像(TextureRect
  3. 2D ゲーム内の「動く床」などの Node2D オブジェクト

手順①: スクリプトをプロジェクトに追加

  1. 上記の GDScript を res://components/hover_scale.gd など好きな場所に保存します。
  2. Godot エディタで開くと、クラス名 HoverScale が Inspector の「ノードを追加」ダイアログに出てくるようになります。

手順②: ボタンに HoverScale をアタッチ

例:メインメニューの「Play」ボタンにホバー拡大を付けたい場合。

MainMenu (Control)
 ├── PlayButton (Button)
 │    └── HoverScale (Node)
 └── QuitButton (Button)
      └── HoverScale (Node)
  1. PlayButton を選択し、右クリック → 「子ノードを追加」。
  2. 検索欄に「HoverScale」と入力し、HoverScale を追加。
  3. 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)
  1. Icon (TextureRect) の子として HoverScale を追加。
  2. hover_scale = 1.15 くらいにして、少しだけ目立たせる。
  3. クリックは普通にアイコン側で処理してOK(HoverScale はスケール変更だけ担当)。

継承したカスタム TextureRect を作らなくても、「HoverScale を付けたらホバー演出あり、付けなければ無し」 というシンプルな構成で管理できます。

手順④: 2D ゲーム内オブジェクト(Node2D)に適用

ゲーム内の「動く床」や「拾えるアイテム」に、マウスオーバーでちょっとだけ大きくなる演出を付けたい場合:

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── HoverScale (Node)

PickupItem (Node2D)
 ├── Sprite2D
 ├── Area2D
 │    └── CollisionShape2D
 └── HoverScale (Node)
  • MovingPlatformPickupItem の子に 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 演出をすべてコンポーネントの組み合わせで表現できるようになります。継承ツリーに悩まされる前に、ぜひ合成ベースの設計を試してみてください。