【Godot 4】OutlineHighlight (輪郭強調) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Godotで「マウスオーバーしたオブジェクトを光らせたい」と思ったとき、ありがちなのが:

  • 各シーンごとに mouse_entered / mouse_exited を書く
  • Sprite2D / MeshInstance3D に直接スクリプトを継承してくっつける
  • ノード階層の奥深くにある Sprite2D を毎回 $Sprite2D で探しにいく

こういう「継承+深いノード探索」スタイルは、最初は楽なんですが、

  • 似たようなコードがあちこちにコピペされる
  • Sprite2D → AnimatedSprite2D に差し替えた瞬間にパスが変わって壊れる
  • 3Dに拡張したくなったときに、また一から書き直し

…と、スケールしたときに地味にツラくなってきます。

そこで「継承より合成」の出番です。
「OutlineHighlight」コンポーネントを 1 個アタッチするだけで、

  • マウスオーバーされたときだけ 2px の白いアウトライン
  • Sprite2D / MeshInstance3D どちらにも対応(マテリアルさえあればOK)
  • シーン構造はシンプルなまま

という仕組みを、きれいに「後付け」できるようにしてみましょう。

【Godot 4】マウスオーバーでキラッと縁取り!「OutlineHighlight」コンポーネント

このコンポーネントは:

  • 対象ノードの マテリアルを差し替えず、インスタンス化して上書き
  • マウスオーバー時だけ outline_enabledtrue にする
  • マウスが離れたら自動で元に戻す

という「マテリアル制御専用コンポーネント」です。
マウス検出は 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)
  1. ① コンポーネントを追加
    Player シーンを開き、ルートの CharacterBody2D の子として
    OutlineHighlight を追加します(+ ノードを追加 → 検索)。
  2. ② 対象ノードを指定
    • target_2d_pathSprite2D をドラッグ&ドロップ。
    • shader_resource は空でもOK(スクリプトが自動生成)。
    • outline_colorColor(1, 1, 1, 1)(白)のままでOK。
    • outline_thickness2.0 に設定。
  3. ③ マウス検出用の 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)
  1. ④ 実行して確認
    ゲームを実行し、プレイヤーにマウスカーソルを重ねると、
    Sprite2D に 2px の白いアウトラインが表示されます。

例2:クリック可能な宝箱にだけアウトラインを付ける

Chest (Area2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── OutlineHighlight (Node)
  1. Chest シーンを作成し、ルートを Area2D にします。
  2. 子として Sprite2DCollisionShape2D を追加します。
  3. さらに子として OutlineHighlight を追加し、target_2d_pathSprite2D を指定。
  4. 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)
  1. 3Dシーンで PickupItem を作成し、ルートを Area3D にします。
  2. 子として MeshInstance3DCollisionShape3D を追加します。
  3. OutlineHighlight を子として追加し、target_3d_pathMeshInstance3D を指定。
  4. Area3Dmouse_entered / mouse_exited(3Dでは input_event を使うことも多いです)から set_hovered() を呼び出します。

このように、「マウスが乗った/離れた」を検出するノードと、
「どう光らせるか」を担当するコンポーネントを分離しておくと、
後からクリック判定のロジックを変えても、アウトライン部分は一切触らなくて済みます。


メリットと応用

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

  • シーン構造がスッキリ
    各オブジェクトのルートスクリプトに「アウトライン用の変数や処理」を書かなくてよくなります。
    「光らせたいなら、このコンポーネントを 1 個足すだけ」というルールにできます。
  • 2D/3Dをまたいで再利用できる
    target_2d_pathtarget_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 にしておけば、
もっと滑らかなアニメーション表現もできますね。

こんな感じで、アウトラインの表現はどんどん差し替えていけるので、
「見た目のチューニング」はこのコンポーネント内だけで完結させておくと、
ゲーム全体のコードがかなりスッキリしてきます。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!