Godotで「マウスオーバーしたオブジェクトを光らせたい」と思ったとき、ありがちなのが:
- 各シーンごとに
mouse_entered/mouse_exitedを書く - Sprite2D / MeshInstance3D に直接スクリプトを継承してくっつける
- ノード階層の奥深くにある
Sprite2Dを毎回$Sprite2Dで探しにいく
こういう「継承+深いノード探索」スタイルは、最初は楽なんですが、
- 似たようなコードがあちこちにコピペされる
- Sprite2D → AnimatedSprite2D に差し替えた瞬間にパスが変わって壊れる
- 3Dに拡張したくなったときに、また一から書き直し
…と、スケールしたときに地味にツラくなってきます。
そこで「継承より合成」の出番です。
「OutlineHighlight」コンポーネントを 1 個アタッチするだけで、
- マウスオーバーされたときだけ 2px の白いアウトライン
- Sprite2D / MeshInstance3D どちらにも対応(マテリアルさえあればOK)
- シーン構造はシンプルなまま
という仕組みを、きれいに「後付け」できるようにしてみましょう。
【Godot 4】マウスオーバーでキラッと縁取り!「OutlineHighlight」コンポーネント
このコンポーネントは:
- 対象ノードの マテリアルを差し替えず、インスタンス化して上書き
- マウスオーバー時だけ
outline_enabledをtrueにする - マウスが離れたら自動で元に戻す
という「マテリアル制御専用コンポーネント」です。
マウス検出は Area2D / CollisionObject2D / Area3D のシグナルに任せつつ、
「光らせるロジック」だけをこのコンポーネントに閉じ込めます。
GDScriptフルコード(OutlineHighlight.gd)
extends Node
class_name OutlineHighlight
## マウスオーバーされたときに、指定したビジュアルノードに
## 「2pxの白いアウトラインシェーダー」をかけるコンポーネント。
##
## 想定ターゲット:
## - 2D: Sprite2D / AnimatedSprite2D / TextureRect など
## - 3D: MeshInstance3D など (material_override を持つもの)
##
## ポイント:
## - 親ノードのマテリアルを複製してから上書きするので、
## 他のインスタンスに影響しない
## - マウスオーバーの検出は、別の Area2D / Area3D などに任せる設計
## →「光らせる処理」だけをコンポーネントとして再利用できる
@export_node_path("CanvasItem") var target_2d_path: NodePath
## 2D用: アウトラインをかけたい CanvasItem (Sprite2D / TextureRect など) へのパス。
## 空の場合は、親ノードを CanvasItem として扱おうとします。
@export_node_path("GeometryInstance3D") var target_3d_path: NodePath
## 3D用: アウトラインをかけたい MeshInstance3D などへのパス。
## 2Dと3Dの両方を指定することもできますが、通常はどちらか片方でOKです。
@export var shader_resource: Shader
## 使用するシェーダーリソース。
## - ここに Outline 用の Shader を指定します。
## - 未指定の場合、このスクリプト内で簡易シェーダーを生成します。
@export var outline_color: Color = Color.WHITE
## アウトラインの色。デフォルトは白。
@export var outline_thickness: float = 2.0
## アウトラインの太さ(ピクセル)。2pxがデフォルト。
@export var start_enabled: bool = false
## シーン開始時からアウトラインを有効にするかどうか。
## デバッグ時に便利です。
var _target_2d: CanvasItem
var _target_3d: GeometryInstance3D
var _material_2d: ShaderMaterial
var _material_3d: ShaderMaterial
var _is_hovered: bool = false
func _ready() -> void:
# 2Dターゲットの解決
if target_2d_path != NodePath():
_target_2d = get_node_or_null(target_2d_path)
elif owner is CanvasItem:
# パス未指定なら、オーナー(親シーンのルート)を CanvasItem として扱う
_target_2d = owner as CanvasItem
# 3Dターゲットの解決
if target_3d_path != NodePath():
_target_3d = get_node_or_null(target_3d_path)
elif owner is GeometryInstance3D:
_target_3d = owner as GeometryInstance3D
# マテリアルを用意
_setup_materials()
# 初期状態
_set_outline_enabled(start_enabled)
func _setup_materials() -> void:
# シェーダーが未指定なら、簡易アウトラインシェーダーをその場で生成
if shader_resource == null:
shader_resource = _create_default_outline_shader()
# 2D用マテリアルの準備
if _target_2d:
var base_material := _target_2d.material
if base_material is ShaderMaterial:
# 既存の ShaderMaterial を複製して使う
_material_2d = base_material.duplicate() as ShaderMaterial
else:
# 新規に ShaderMaterial を作成
_material_2d = ShaderMaterial.new()
_material_2d.shader = shader_resource
_apply_outline_uniforms(_material_2d)
_target_2d.material = _material_2d
# 3D用マテリアルの準備
if _target_3d:
var base_material3d := _target_3d.material_override
if base_material3d is ShaderMaterial:
_material_3d = base_material3d.duplicate() as ShaderMaterial
else:
_material_3d = ShaderMaterial.new()
_material_3d.shader = shader_resource
_apply_outline_uniforms(_material_3d)
_target_3d.material_override = _material_3d
func _apply_outline_uniforms(mat: ShaderMaterial) -> void:
# シェーダー側の uniform に値を流し込むヘルパー
if not mat:
return
if mat.shader and mat.shader.has_param("outline_color"):
mat.set_shader_parameter("outline_color", outline_color)
if mat.shader and mat.shader.has_param("outline_thickness"):
mat.set_shader_parameter("outline_thickness", outline_thickness)
if mat.shader and mat.shader.has_param("outline_enabled"):
mat.set_shader_parameter("outline_enabled", start_enabled)
func set_outline_color(color: Color) -> void:
outline_color = color
if _material_2d:
_material_2d.set_shader_parameter("outline_color", outline_color)
if _material_3d:
_material_3d.set_shader_parameter("outline_color", outline_color)
func set_outline_thickness(thickness: float) -> void:
outline_thickness = thickness
if _material_2d:
_material_2d.set_shader_parameter("outline_thickness", outline_thickness)
if _material_3d:
_material_3d.set_shader_parameter("outline_thickness", outline_thickness)
func set_hovered(hovered: bool) -> void:
## 外部から「マウスが乗った/外れた」を通知するためのAPI。
_is_hovered = hovered
_set_outline_enabled(_is_hovered)
func _set_outline_enabled(enabled: bool) -> void:
if _material_2d and _material_2d.shader and _material_2d.shader.has_param("outline_enabled"):
_material_2d.set_shader_parameter("outline_enabled", enabled)
if _material_3d and _material_3d.shader and _material_3d.shader.has_param("outline_enabled"):
_material_3d.set_shader_parameter("outline_enabled", enabled)
func _create_default_outline_shader() -> Shader:
## シンプルな 2D/3D 兼用アウトラインシェーダーを生成します。
## ※2Dではテクスチャのアルファをもとにアウトラインを描画します。
## 3Dではスクリーンスペースでの簡易アウトライン風になります。
var shader_code := """
shader_type canvas_item;
uniform bool outline_enabled = false;
uniform vec4 outline_color : source_color = vec4(1.0);
uniform float outline_thickness = 2.0;
void fragment() {
vec4 tex = texture(TEXTURE, UV);
if (!outline_enabled) {
COLOR = tex;
return;
}
// 元のピクセルが不透明ならそのまま表示
if (tex.a > 0.0) {
COLOR = tex;
return;
}
// 周囲のピクセルをチェックして、どこかが不透明ならアウトライン色に
float px = outline_thickness / float(textureSize(TEXTURE, 0).x);
float py = outline_thickness / float(textureSize(TEXTURE, 0).y);
float alpha_sum = 0.0;
alpha_sum += texture(TEXTURE, UV + vec2( px, 0.0)).a;
alpha_sum += texture(TEXTURE, UV + vec2(-px, 0.0)).a;
alpha_sum += texture(TEXTURE, UV + vec2( 0.0, py)).a;
alpha_sum += texture(TEXTURE, UV + vec2( 0.0,-py)).a;
if (alpha_sum > 0.0) {
COLOR = outline_color;
} else {
COLOR = tex;
}
}
"""
var shader := Shader.new()
shader.code = shader_code
return shader
このスクリプトを OutlineHighlight.gd というファイル名で保存しておけば、
エディタ上で コンポーネントとして追加できるようになります。
使い方の手順
ここでは 2D のプレイヤーと、クリック可能な宝箱を例にしてみます。
例1:プレイヤーを常時アウトライン(デバッグ用)+マウスオーバーで変化
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── HitArea (Area2D) │ └── CollisionShape2D └── OutlineHighlight (Node)
-
① コンポーネントを追加
Player シーンを開き、ルートのCharacterBody2Dの子として
OutlineHighlightを追加します(+ ノードを追加→ 検索)。 -
② 対象ノードを指定
target_2d_pathにSprite2Dをドラッグ&ドロップ。shader_resourceは空でもOK(スクリプトが自動生成)。outline_colorはColor(1, 1, 1, 1)(白)のままでOK。outline_thicknessは2.0に設定。
-
③ マウス検出用の Area2D をつなぐ
すでにあるHitArea (Area2D)からシグナルを飛ばします。
HitAreaを選択 → 「ノード」タブ → シグナル一覧から
mouse_entered/mouse_exitedを選び、
Player ルート(CharacterBody2D)に接続して、以下のように書きます。
# Player.gd (CharacterBody2D のスクリプト)
@onready var outline: OutlineHighlight = $OutlineHighlight
func _on_HitArea_mouse_entered() -> void:
outline.set_hovered(true)
func _on_HitArea_mouse_exited() -> void:
outline.set_hovered(false)
-
④ 実行して確認
ゲームを実行し、プレイヤーにマウスカーソルを重ねると、
Sprite2D に 2px の白いアウトラインが表示されます。
例2:クリック可能な宝箱にだけアウトラインを付ける
Chest (Area2D) ├── Sprite2D ├── CollisionShape2D └── OutlineHighlight (Node)
- Chest シーンを作成し、ルートを
Area2Dにします。 - 子として
Sprite2DとCollisionShape2Dを追加します。 - さらに子として
OutlineHighlightを追加し、target_2d_pathにSprite2Dを指定。 Area2D自体にスクリプトChest.gdをアタッチし、mouse_entered/mouse_exitedを接続して以下のようにします。
# Chest.gd (Area2D)
@onready var outline: OutlineHighlight = $OutlineHighlight
func _on_mouse_entered() -> void:
outline.set_hovered(true)
func _on_mouse_exited() -> void:
outline.set_hovered(false)
これで、宝箱の上にマウスを乗せたときだけ、輪郭がふわっと強調されるようになります。
例3:3Dのオブジェクトに適用する場合
PickupItem (Area3D) ├── MeshInstance3D ├── CollisionShape3D └── OutlineHighlight (Node)
- 3Dシーンで
PickupItemを作成し、ルートをArea3Dにします。 - 子として
MeshInstance3DとCollisionShape3Dを追加します。 OutlineHighlightを子として追加し、target_3d_pathにMeshInstance3Dを指定。Area3Dのmouse_entered/mouse_exited(3Dではinput_eventを使うことも多いです)からset_hovered()を呼び出します。
このように、「マウスが乗った/離れた」を検出するノードと、
「どう光らせるか」を担当するコンポーネントを分離しておくと、
後からクリック判定のロジックを変えても、アウトライン部分は一切触らなくて済みます。
メリットと応用
この OutlineHighlight コンポーネントを使うことで:
- シーン構造がスッキリ
各オブジェクトのルートスクリプトに「アウトライン用の変数や処理」を書かなくてよくなります。
「光らせたいなら、このコンポーネントを 1 個足すだけ」というルールにできます。 - 2D/3Dをまたいで再利用できる
target_2d_pathとtarget_3d_pathを切り替えるだけで、
基本的な使い方はまったく同じです。UIボタンだけでなく、3Dオブジェクトにも同じ感覚で適用できます。 - マテリアルの管理が安全
元のマテリアルを複製してから上書きしているので、
他のインスタンスやシーンに影響を与えずにアウトラインを追加できます。 - レベルデザインが楽になる
「インタラクティブなオブジェクトは全部アウトラインを付ける」というルールにしておけば、
レベルデザイナーはシーン上でコンポーネントをポチポチ付けていくだけで視覚的なフィードバックを統一できます。
そして何より、「継承した巨大な Player.gd に全部書く」スタイルから脱却して、
「必要な機能をコンポーネントとして足していく」スタイルに移行しやすくなります。
改造案:フェードイン/フェードアウトするアウトライン
マウスが乗った瞬間にパッと切り替わるのではなく、
ふわっとフェードするようにしたい場合は、OutlineHighlight にこんな関数を追加するのもアリです。
func animate_hover(hovered: bool, duration: float = 0.15) -> void:
## アウトラインの有効/無効を、ふわっとアニメーションさせる。
_is_hovered = hovered
var start := _material_2d.get_shader_parameter("outline_enabled") if _material_2d else (hovered ? 0.0 : 1.0)
var end := hovered ? 1.0 : 0.0
var tween := create_tween()
tween.tween_method(
func(value: float) -> void:
if _material_2d and _material_2d.shader.has_param("outline_enabled"):
_material_2d.set_shader_parameter("outline_enabled", value > 0.01)
,
start, end, duration
)
シェーダー側を outline_strength のような float にしておけば、
もっと滑らかなアニメーション表現もできますね。
こんな感じで、アウトラインの表現はどんどん差し替えていけるので、
「見た目のチューニング」はこのコンポーネント内だけで完結させておくと、
ゲーム全体のコードがかなりスッキリしてきます。




