Godot 4で「ホバーしたら輪郭線を光らせたい!」みたいな演出、よくやりたくなりますよね。
でも素直にやろうとすると、

  • 各シーンごとに Sprite2D にマウス判定用のスクリプトを書く
  • 共通のシェーダーを継承で持たせたり、子ノードに専用ノードを増やしたり
  • 「プレイヤー用」「敵用」「アイテム用」と微妙に違うスクリプトが乱立

……といった感じで、どんどんスクリプトやノード構造が肥大化しがちです。
特に「ホバーしたら輪郭線ON/OFFするだけ」のために、ベースクラスを作って継承させるのはちょっと大げさですよね。

そこでこの記事では、親の Sprite2D のマテリアルをいじって輪郭線を出すだけに特化したコンポーネント
OutlineShader コンポーネントを紹介します。

どのシーンにも「ペタッ」と貼るだけで、継承なし・ノード構造の変更なしでホバー輪郭線を実現できます。
まさに「継承より合成(Composition)」なアプローチですね。

【Godot 4】ホバーで光る輪郭線!「OutlineShader」コンポーネント

このコンポーネントは、

  • 親ノードの Sprite2D(または CanvasItem)のマテリアルを操作
  • マウスホバー時にシェーダーパラメータを変更して輪郭線(アウトライン)を表示
  • ホバー解除で自動的に元に戻す

という、かなり一点特化の小さなノードです。
プレイヤー、敵、アイテム、UIボタンなど、どこにでも同じノードをアタッチして使い回せます。


前提:シェーダーマテリアルについて

この記事のコンポーネントは、アウトライン描画用の ShaderMaterial を前提にしています。
「とりあえず動かしたい」方のために、簡単なサンプルシェーダーも後半に載せておきます。


OutlineShader.gd(フルコード)


extends Node
class_name OutlineShader
"""
OutlineShader コンポーネント

親の Sprite2D / CanvasItem の ShaderMaterial を操作して、
マウスホバー時に輪郭線をON/OFFするためのコンポーネントです。

・親ノードに ShaderMaterial を設定しておき、
  アウトライン用のパラメータ(例: "outline_enabled")を持たせておきます。
・このコンポーネントは、そのパラメータを切り替えるだけに専念します。

「継承して各キャラごとにホバー処理を書く」のではなく、
必要なノードにこのコンポーネントをポン付けする想定です。
"""

# =========================
# エクスポートパラメータ
# =========================

@export var target_node: CanvasItem:
	"""
	輪郭線を操作したい描画ノード。
	通常は Sprite2D / TextureRect などの CanvasItem を指定します。
	未指定の場合は、自動的に親ノードを CanvasItem として解釈しようとします。
	"""
	get:
		return target_node
	set(value):
		target_node = value
		_update_references()

@export var shader_param_enabled_name: StringName = &"outline_enabled":
	"""
	シェーダー側で「輪郭線ON/OFF」を制御する bool パラメータ名。
	例: uniform bool outline_enabled;
	"""
	get:
		return shader_param_enabled_name
	set(value):
		shader_param_enabled_name = value
		_apply_outline_state()

@export var shader_param_color_name: StringName = &"outline_color":
	"""
	輪郭線の色を制御する Color パラメータ名。
	例: uniform vec4 outline_color : source_color;
	"""
	get:
		return shader_param_color_name
	set(value):
		shader_param_color_name = value
		_apply_outline_state()

@export var shader_param_thickness_name: StringName = &"outline_thickness":
	"""
	輪郭線の太さを制御する float パラメータ名。
	ピクセル単位など、シェーダーの実装に合わせてください。
	"""
	get:
		return shader_param_thickness_name
	set(value):
		shader_param_thickness_name = value
		_apply_outline_state()

@export var hover_outline_color: Color = Color(1, 1, 0, 1):
	"""
	ホバー時に設定する輪郭線の色。
	デフォルトは黄色。
	"""
	get:
		return hover_outline_color
	set(value):
		hover_outline_color = value
		if _is_hovered:
			_apply_outline_state()

@export var hover_outline_thickness: float = 2.0:
	"""
	ホバー時に設定する輪郭線の太さ。
	"""
	get:
		return hover_outline_thickness
	set(value):
		hover_outline_thickness = value
		if _is_hovered:
			_apply_outline_state()

@export var default_outline_enabled: bool = false:
	"""
	ホバーしていない時のデフォルト状態で、輪郭線を有効にしておくか。
	「常にうっすら表示しておいて、ホバーで強調」みたいな使い方が可能です。
	"""
	get:
		return default_outline_enabled
	set(value):
		default_outline_enabled = value
		if not _is_hovered:
			_apply_outline_state()

@export var default_outline_color: Color = Color(1, 1, 1, 0.0):
	"""
	ホバーしていない時の輪郭線の色。
	完全に消したい場合は alpha=0 にするか、enabled=false にします。
	"""
	get:
		return default_outline_color
	set(value):
		default_outline_color = value
		if not _is_hovered:
			_apply_outline_state()

@export var default_outline_thickness: float = 0.0:
	"""
	ホバーしていない時の輪郭線の太さ。
	"""
	get:
		return default_outline_thickness
	set(value):
		default_outline_thickness = value
		if not _is_hovered:
			_apply_outline_state()

@export var require_left_click_focus: bool = false:
	"""
	true の場合:
	  ・ホバーしていても、左クリックでフォーカスを取るまでは輪郭線を有効にしない
	  ・左クリック中だけ輪郭線を強調する…などのUI的な使い方が可能
	この記事ではオプションなので、基本は false のままでOKです。
	"""
	get:
		return require_left_click_focus
	set(value):
		require_left_click_focus = value

# =========================
# 内部状態
# =========================

var _canvas_item: CanvasItem
var _shader_material: ShaderMaterial
var _is_hovered: bool = false
var _has_focus_click: bool = false


func _ready() -> void:
	"""
	初期化。ターゲットノードとシェーダーへの参照を取得します。
	親ノードが CanvasItem なら、それを自動的にターゲットにします。
	"""
	if target_node == null:
		# 親が CanvasItem なら自動で採用
		if owner is CanvasItem:
			target_node = owner
		elif get_parent() is CanvasItem:
			target_node = get_parent()
	_update_references()
	_apply_outline_state()
	# マウスイベントを受け取るために、親側のマウスフィルタ設定を確認しておく
	_enable_mouse_pickable_if_possible()


func _update_references() -> void:
	"""
	target_node の変更時に呼ばれ、CanvasItem および ShaderMaterial への参照を更新します。
	"""
	_canvas_item = null
	_shader_material = null

	if target_node and target_node is CanvasItem:
		_canvas_item = target_node
	elif get_parent() is CanvasItem:
		_canvas_item = get_parent()

	if _canvas_item:
		# CanvasItem.material が ShaderMaterial であることを期待
		if _canvas_item.material is ShaderMaterial:
			_shader_material = _canvas_item.material
		else:
			push_warning("OutlineShader: target CanvasItem has no ShaderMaterial. Outline will not work.")
	else:
		push_warning("OutlineShader: No valid CanvasItem target found.")


func _enable_mouse_pickable_if_possible() -> void:
	"""
	親ノードが Control / Node2D 系の場合に、マウスイベントを拾えるように設定を補助します。
	必要に応じて、CollisionShape2D などはユーザー側で用意してください。
	"""
	if _canvas_item == null:
		return

	# Control の場合は mouse_filter を調整
	if _canvas_item is Control:
		var c := _canvas_item as Control
		if c.mouse_filter == Control.MOUSE_FILTER_IGNORE:
			c.mouse_filter = Control.MOUSE_FILTER_STOP

	# Sprite2D / Node2D の場合は、CollisionShape2D + input_pickable が必要になるケースが多いです。
	# ここでは自動化しませんが、注意喚起として警告を出しておきます。
	if _canvas_item is Sprite2D:
		if not (_canvas_item as Sprite2D).input_pickable:
			push_warning("OutlineShader: Sprite2D.input_pickable is false. Hover detection may not work without a CollisionShape2D + Area2D or enabling input_pickable.")


func _unhandled_input(event: InputEvent) -> void:
	"""
	クリック系の入力を拾って、require_left_click_focus オプションに対応します。
	"""
	if not require_left_click_focus:
		return

	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
		if event.pressed and _is_hovered:
			_has_focus_click = true
			_apply_outline_state()
		elif not event.pressed:
			_has_focus_click = false
			_apply_outline_state()


func _on_mouse_entered() -> void:
	_is_hovered = true
	_apply_outline_state()


func _on_mouse_exited() -> void:
	_is_hovered = false
	_has_focus_click = false
	_apply_outline_state()


func _apply_outline_state() -> void:
	"""
	現在の状態(ホバー中かどうか、クリックフォーカスが必要かどうか)に応じて、
	ShaderMaterial のパラメータを更新します。
	"""
	if _shader_material == null:
		return

	var enabled := default_outline_enabled
	var color := default_outline_color
	var thickness := default_outline_thickness

	if _is_hovered:
		if require_left_click_focus:
			# 左クリックでフォーカスを取っている時だけ強調表示
			if _has_focus_click:
				enabled = true
				color = hover_outline_color
				thickness = hover_outline_thickness
			else:
				# フォーカス前はデフォルト状態
				enabled = default_outline_enabled
				color = default_outline_color
				thickness = default_outline_thickness
		else:
			# 単純にホバー中は強調表示
			enabled = true
			color = hover_outline_color
			thickness = hover_outline_thickness

	# 実際にシェーダーパラメータを設定
	if shader_param_enabled_name != StringName():
		if _shader_material.get_shader_parameter(shader_param_enabled_name) != null:
			_shader_material.set_shader_parameter(shader_param_enabled_name, enabled)
	if shader_param_color_name != StringName():
		if _shader_material.get_shader_parameter(shader_param_color_name) != null:
			_shader_material.set_shader_parameter(shader_param_color_name, color)
	if shader_param_thickness_name != StringName():
		if _shader_material.get_shader_parameter(shader_param_thickness_name) != null:
			_shader_material.set_shader_parameter(shader_param_thickness_name, thickness)


func _enter_tree() -> void:
	"""
	親ノードの mouse_entered / mouse_exited シグナルに自動接続します。
	"""
	# Hover 判定は「描画ノード」側のシグナルを使うのが自然なので、
	# target_node または親 CanvasItem のシグナルに接続します。
	await get_tree().process_frame  # 親が揃うのを待つ
	_update_references()

	if _canvas_item:
		# すでに接続済みかどうかチェック
		if not _canvas_item.is_connected("mouse_entered", Callable(self, "_on_mouse_entered")):
			_canvas_item.mouse_entered.connect(_on_mouse_entered)
		if not _canvas_item.is_connected("mouse_exited", Callable(self, "_on_mouse_exited")):
			_canvas_item.mouse_exited.connect(_on_mouse_exited)

サンプル:アウトライン用シェーダー

最低限動かすための、非常にシンプルな 2D アウトラインシェーダー例です。
(高品質なアウトラインが欲しい場合は、ここを各自で強化してください。)


# ファイル: outline.shader
shader_type canvas_item;

uniform bool outline_enabled = false;
uniform float outline_thickness = 2.0;
uniform vec4 outline_color : source_color = vec4(1.0, 1.0, 0.0, 1.0);

void fragment() {
	vec4 tex = texture(TEXTURE, UV);
	if (!outline_enabled) {
		COLOR = tex;
		return;
	}

	// ざっくり 8 方向サンプリングでアウトラインを描画
	float alpha = tex.a;
	float max_alpha = alpha;
	for (int x = -1; x <= 1; x++) {
		for (int y = -1; y <= 1; y++) {
			if (x == 0 && y == 0) continue;
			vec2 offset = vec2(float(x), float(y)) * outline_thickness / TEXTURE_PIXEL_SIZE;
			vec4 sample_tex = texture(TEXTURE, UV + offset);
			max_alpha = max(max_alpha, sample_tex.a);
		}
	}

	if (alpha > 0.0) {
		// 本体ピクセル
		COLOR = tex;
	} else if (max_alpha > 0.0) {
		// 周囲にピクセルがある=輪郭
		COLOR = outline_color;
	} else {
		// 完全に透明
		COLOR = vec4(0.0);
	}
}

このシェーダーを ShaderMaterial に設定し、
OutlineShader コンポーネントのパラメータ名をデフォルトのまま使えば、すぐ動きます。


使い方の手順

① シェーダーマテリアルを用意する

  1. Godot で新規リソース > Shader を作成し、ファイル名を outline.shader にする。
  2. 上記のシェーダーコードをコピペ。
  3. 同じく新規リソース > ShaderMaterial を作成し、Shaderoutline.shader を指定。

この ShaderMaterial を、輪郭線を出したい Sprite2DMaterial に設定します。

② OutlineShader コンポーネントをシーンに追加する

例として「プレイヤーキャラ」にアウトラインを付けるシーン構成はこんな感じです:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── OutlineShader (Node)
  1. Player シーンを開く。
  2. Sprite2DMaterial に、さきほど作った ShaderMaterial を設定。
  3. Player の子として Node を追加し、スクリプトに OutlineShader.gd をアタッチ。
  4. インスペクタで target_nodeSprite2D に設定(または未設定なら自動で親を拾います)。

この時、ホバー判定のために以下のどちらかを用意してください:

  • Sprite2D.input_pickable = true にする(簡易)
  • Area2D + CollisionShape2D を別途用意して、そちらの mouse_entered / mouse_exited に連動させる(高精度)

この記事のコンポーネントは Sprite2D.mouse_entered を使う前提なので、
まずは Sprite2D.input_pickable = true をオンにするのがおすすめです。

③ 輪郭線の色や太さを調整する

OutlineShader ノードを選択し、インスペクタから:

  • hover_outline_color … ホバー中の色(例:黄色、青色など)
  • hover_outline_thickness … ホバー中の太さ(ピクセル相当)
  • default_outline_enabled … 通常時も薄く輪郭を出したい場合に true
  • default_outline_color / default_outline_thickness … 通常時の見た目

たとえば「通常は薄い白い輪郭、ホバーで黄色く強調」といった演出も簡単にできます。

④ 他のオブジェクトにもコピペで使い回す

敵キャラやアイテムにも、まったく同じコンポーネントを貼るだけでOKです。

例:敵キャラシーン

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── OutlineShader (Node)

例:拾えるアイテムシーン

Item (Area2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── OutlineShader (Node)

どのシーンでも、同じ OutlineShader コンポーネントを使い回せるので、
「敵用ホバー処理」「アイテム用ホバー処理」といったコピペ地獄から解放されます。


メリットと応用

このコンポーネントを使うことで、

  • ノード構造を増やさずに演出を足せる
    Sprite2D の子にさらに装飾用ノードを増やす必要がなく、シーンツリーがスッキリします。
  • 継承ベースの共通クラスが不要
    「ホバーで光るプレイヤー」「ホバーで光る敵」などを、HoverHighlightBase みたいな基底クラスでまとめる必要がありません。
  • アセットの再利用が簡単
    ShaderMaterial と OutlineShader コンポーネントをセットでプリセット化しておけば、新しいオブジェクトにも数クリックで導入できます。
  • レベルデザイン時の視認性アップ
    拾えるアイテムやインタラクト可能なオブジェクトだけに輪郭線を付けることで、プレイヤーにとって「触れるもの」が一目で分かります。

「描画(シェーダー)」と「いつ光らせるか(ロジック)」を分離しているので、
シェーダーを差し替えるだけで、グロー、色反転、点滅など別の演出にも簡単に応用できます。

改造案:点滅しながらホバーアウトラインを出す

例えば「ホバー中は輪郭線が点滅する」ようにしたい場合、
コンポーネント側で時間を使って太さを揺らすだけでもそれっぽくなります。


# OutlineShader.gd に追記(簡易な点滅エフェクト)
@export var pulse_on_hover: bool = false
@export var pulse_speed: float = 6.0
@export var pulse_amplitude: float = 1.0

var _time_accum: float = 0.0

func _process(delta: float) -> void:
	if not pulse_on_hover:
		return
	if not _is_hovered:
		return
	if _shader_material == null:
		return

	_time_accum += delta * pulse_speed
	var pulse := sin(_time_accum) * 0.5 + 0.5  # 0.0 ~ 1.0
	var base_thickness := hover_outline_thickness
	var t := base_thickness + pulse * pulse_amplitude
	if shader_param_thickness_name != StringName():
		if _shader_material.get_shader_parameter(shader_param_thickness_name) != null:
			_shader_material.set_shader_parameter(shader_param_thickness_name, t)

このように、コンポーネント一つを改造するだけで、全てのオブジェクトの演出を一括強化できるのが、
「継承より合成」スタイルの一番おいしいところですね。

ぜひ、自分のプロジェクト用に OutlineShader をカスタマイズして、
「ホバーで光るオブジェクト」を量産してみてください。