キャラの足元に置く「丸影」、Godotでどう作っていますか?
多くの人は、プレイヤーシーンの子ノードに Sprite2D を置いて、position.y をスクリプトから直接いじったり、CharacterBody2D を継承したプレイヤークラスの中に「影ロジック」まで全部書いてしまいがちです。

でもこのやり方だと、

  • プレイヤー・敵・動く足場など、影が欲しいオブジェクトごとに同じロジックをコピペしがち
  • プレイヤーのスクリプトが「移動」「アニメーション」「影制御」でどんどん肥大化する
  • 別のゲームで使い回したい時に、継承関係やノード構造の違いで移植が面倒

そこで今回は「どんなノードにもポン付けで使える」コンポーネントとして、ShadowBlob(丸影)コンポーネントを用意しました。
「キャラが浮いたら影が地面に残る」「段差に合わせて影の高さが変わる」「ジャンプ中は影が小さくなる」などを、RayCast2D で地面を検出しつつ、楕円形の影スプライトを自動配置して実現します。

【Godot 4】足元の丸影をコンポーネント化!「ShadowBlob」コンポーネント

このコンポーネントは、ざっくり言うと:

  • 親ノードの真下に RayCast2D を飛ばして地面の高さを取得
  • ヒットした位置に「楕円形の影」を自動配置
  • 親ノードとの距離に応じて、影のスケールや不透明度を自動調整

という仕事をしてくれる「足元の影マネージャ」です。
どのノードにでも子としてアタッチできるように作ってあるので、継承ではなく合成(Composition)で影機能を付け足せます。


GDScript フルコード


extends Node2D
class_name ShadowBlob
## ShadowBlob: 足元の丸影コンポーネント
##
## 親ノードの真下に RayCast2D を飛ばし、地面に合わせて楕円形の影を置く。
## - 親ノード: 位置の基準(プレイヤー、敵、動く床など)
## - RayCast2D: 地面の高さ検出
## - Sprite2D: 楕円形の影テクスチャ表示
##
## どんな Node2D/CharacterBody2D などにも子としてアタッチして使える。

@export_group("RayCast 設定")
## RayCast の長さ(親の足元からどこまで下に飛ばすか)
@export var cast_length: float = 200.0

## RayCast のローカル開始オフセット(親の足元が少し下にある場合など)
@export var cast_start_offset: Vector2 = Vector2(0, 0)

## どのコリジョンレイヤーを地面とみなすか(PhysicsLayer ビットフラグ)
@export var collision_mask: int = 1

@export_group("影の見た目")
## 影として使うテクスチャ(楕円っぽい画像を推奨)
@export var shadow_texture: Texture2D

## 影の基本スケール(親が地面に近い時の大きさ)
@export var base_scale: Vector2 = Vector2(1.0, 0.5)

## 影の最小スケール(親が高く浮いた時の最小サイズ)
@export var min_scale: Vector2 = Vector2(0.4, 0.2)

## 影の最大スケール(地面にめり込ませたい時などに調整)
@export var max_scale: Vector2 = Vector2(1.2, 0.6)

## 影の基本色(アルファ込み)。黒に近い半透明が無難。
@export var shadow_color: Color = Color(0, 0, 0, 0.6)

@export_group("高さによる変化")
## 親と地面の距離がこの値以上になると「最大高さ」とみなす
@export var max_height_for_scale: float = 200.0

## 高さに応じて影のアルファを減らすかどうか
@export var fade_with_height: bool = true

## 親がこの高さ以上にいる時、影のアルファを何倍にするか(0 で完全透明)
@export_range(0.0, 1.0, 0.01)
@export var min_alpha_factor: float = 0.2

@export_group("動作オプション")
## 地面が見つからない時に影を非表示にするか
@export var hide_when_no_ground: bool = true

## RayCast の結果をデバッグ表示(エディタ/実行中ともに)
@export var debug_draw: bool = false

## RayCast2D を自動生成するか。
## false にして自前の RayCast2D を子ノードとして置き、パラメータを細かく調整することも可能。
@export var auto_create_raycast: bool = true

## 影 Sprite2D を自動生成するか。
## false にして自前の Sprite2D を子ノードとして置き、テクスチャやマテリアルを細かく調整することも可能。
@export var auto_create_sprite: bool = true


var _raycast: RayCast2D
var _shadow_sprite: Sprite2D


func _ready() -> void:
    _setup_raycast()
    _setup_shadow_sprite()


func _setup_raycast() -> void:
    # すでに子に RayCast2D がある場合はそれを優先して使う
    var existing: RayCast2D = get_node_or_null("ShadowRayCast2D")
    if existing and not auto_create_raycast:
        _raycast = existing
    else:
        if not existing:
            _raycast = RayCast2D.new()
            _raycast.name = "ShadowRayCast2D"
            add_child(_raycast)
        else:
            _raycast = existing

    # RayCast の基本設定
    _raycast.enabled = true
    _raycast.collide_with_areas = false
    _raycast.collide_with_bodies = true
    _raycast.collision_mask = collision_mask

    # 足元から真下に飛ばす
    _update_raycast_shape()


func _setup_shadow_sprite() -> void:
    # すでに子に Sprite2D がある場合はそれを優先して使う
    var existing: Sprite2D = get_node_or_null("ShadowSprite2D")
    if existing and not auto_create_sprite:
        _shadow_sprite = existing
    else:
        if not existing:
            _shadow_sprite = Sprite2D.new()
            _shadow_sprite.name = "ShadowSprite2D"
            add_child(_shadow_sprite)
        else:
            _shadow_sprite = existing

    # Sprite2D の基本設定
    _shadow_sprite.texture = shadow_texture
    _shadow_sprite.modulate = shadow_color
    _shadow_sprite.scale = base_scale
    _shadow_sprite.centered = true
    _shadow_sprite.flip_v = false
    _shadow_sprite.flip_h = false
    _shadow_sprite.show_behind_parent = true  # 影なので親の後ろに描画されるように


func _update_raycast_shape() -> void:
    if not _raycast:
        return
    # RayCast の開始位置(ローカル)
    _raycast.position = cast_start_offset
    # RayCast の終点(ローカル)
    _raycast.target_position = Vector2(0, cast_length)


func _physics_process(delta: float) -> void:
    if not _raycast or not _shadow_sprite:
        return

    _update_raycast_shape()
    _raycast.force_raycast_update()

    if _raycast.is_colliding():
        var hit_pos: Vector2 = _raycast.get_collision_point()
        # 親ノードのグローバル座標
        var parent_pos: Vector2 = get_parent() if get_parent() is Node2D else global_position
        if get_parent() is Node2D:
            parent_pos = (get_parent() as Node2D).global_position

        # 影の位置は「ヒット地点」に合わせる
        global_position = hit_pos

        # 親と地面の高さ差(Y軸)を正の値で扱う
        var height: float = max(parent_pos.y - hit_pos.y, 0.0)

        _update_shadow_scale_and_alpha(height)
        _shadow_sprite.visible = true
    else:
        if hide_when_no_ground:
            _shadow_sprite.visible = false

    if debug_draw:
        update()  # _draw() を呼ぶ


func _update_shadow_scale_and_alpha(height: float) -> void:
    # 高さ 0 ~ max_height_for_scale を 0.0 ~ 1.0 に正規化
    var t: float = 0.0
    if max_height_for_scale > 0.0:
        t = clamp(height / max_height_for_scale, 0.0, 1.0)

    # スケールを線形補間:高さが増えると min_scale に近づく
    var new_scale_x = lerp(base_scale.x, min_scale.x, t)
    var new_scale_y = lerp(base_scale.y, min_scale.y, t)

    # 一応 max_scale の範囲にもクランプ
    new_scale_x = clamp(new_scale_x, min_scale.x, max_scale.x)
    new_scale_y = clamp(new_scale_y, min_scale.y, max_scale.y)
    _shadow_sprite.scale = Vector2(new_scale_x, new_scale_y)

    # アルファも高さに応じて減らす
    if fade_with_height:
        var base_alpha = shadow_color.a
        var alpha_factor = lerp(1.0, min_alpha_factor, t)
        var c = shadow_color
        c.a = base_alpha * alpha_factor
        _shadow_sprite.modulate = c
    else:
        _shadow_sprite.modulate = shadow_color


func _draw() -> void:
    if not debug_draw:
        return

    # RayCast の可視化(エディタ上・実行中の両方で確認できる)
    if _raycast:
        var from: Vector2 = _raycast.position
        var to: Vector2 = _raycast.to_local(_raycast.to_global(_raycast.position + _raycast.target_position))
        draw_line(from, to, Color(0, 1, 0, 0.8), 1.0)

        # ヒット地点のマーカー
        if _raycast.is_colliding():
            var local_hit: Vector2 = to_local(_raycast.get_collision_point())
            draw_circle(local_hit, 4.0, Color(1, 0, 0, 0.8))


## 外部から影の色だけ変えたい時用のヘルパー
func set_shadow_color(color: Color) -> void:
    shadow_color = color
    if _shadow_sprite:
        var c = color
        # fade_with_height が有効な場合は _update_shadow_scale_and_alpha 内で再設定される
        _shadow_sprite.modulate = c


## 外部からテクスチャを差し替えるヘルパー
func set_shadow_texture(tex: Texture2D) -> void:
    shadow_texture = tex
    if _shadow_sprite:
        _shadow_sprite.texture = tex

使い方の手順

ここでは代表的な例として、

  • ① プレイヤー(CharacterBody2D)
  • ② 敵キャラ(CharacterBody2D)
  • ③ 動く足場(StaticBody2D / Node2D)

に ShadowBlob を付ける手順を見ていきます。

手順①:影用テクスチャを用意する

  • 画像編集ソフトで、黒〜グレーの楕円を描いた PNG を作ります。
  • 背景は完全に透明にし、中央寄せの楕円にしておくと扱いやすいです。
  • Godot にインポートしたら、必要に応じて Filter オフ / Mipmaps オフ にしておきましょう(ドット絵なら特に)。

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

  1. res://components/ShadowBlob.gd など、分かりやすい場所に上記コードを保存。
  2. Godot を再読み込みすると、ノード追加ダイアログで ShadowBlob がクラスとして選べるようになります。

手順③:プレイヤーにアタッチする

典型的なプレイヤーシーン構成例:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── ShadowBlob (Node2D)
  1. Player シーンを開き、ルート(CharacterBody2D)を選択。
  2. 右クリック → 「子ノードを追加」 → 検索欄に「ShadowBlob」と入力して追加。
  3. ShadowBlob を選択し、インスペクタで以下を設定:
    • shadow_texture:手順①で作成した楕円テクスチャ
    • cast_length:200 前後(キャラのジャンプの高さに合わせて調整)
    • cast_start_offset(0, 0)(0, 足元までの距離)(Sprite2D の原点が胴体にあるなら少し下げる)
    • collision_mask:地面の PhysicsLayer に合わせる
    • base_scale / min_scale:見た目を見ながら微調整

これで、プレイヤーがジャンプしたり落下したりしても、足元の地面に丸影がピタッと追従するはずです。
段差の上に乗ると影の位置もちゃんと段差の上に移動してくれるので、立体感がかなり出ます。

手順④:敵や動く足場にも再利用する

敵シーン例:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── ShadowBlob (Node2D)

動く足場シーン例:

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D / StaticBody2D
 └── ShadowBlob (Node2D)
  • どちらのシーンでも、やることは 「子ノードとして ShadowBlob を追加するだけ」です。
  • 敵のサイズが違うなら base_scale だけ少し大きめにする、などの調整をします。
  • 動く足場の場合は、fade_with_height をオフにして常に同じ濃さの影にしても良いですね。

このように、プレイヤー・敵・ギミックなど、どのシーンでも同じコンポーネントをポン付けできるのが合成(Composition)の強みです。


メリットと応用

ShadowBlob コンポーネントを使うメリットを整理すると:

  • スクリプトの責務が分離できる
    プレイヤー側は「移動・入力・アニメーション」に集中し、
    影の制御はすべて ShadowBlob に任せられます。
  • シーン構造がシンプルに保てる
    「影専用ノード」が 1 個ぶら下がるだけなので、
    Godot でありがちな「深くて謎なノード階層」を避けられます。
  • 再利用性が高い
    継承ベースだと「Player.gd に影ロジックを書いたけど、Enemy.gd にはコピペ…」となりがちですが、
    コンポーネントならあらゆる Node2D にそのまま使い回せます。
  • レベルデザインが視覚的にやりやすくなる
    影の位置や濃さをインスペクタから調整できるので、
    「このステージは影を薄めにしたい」「このボスだけ影を大きくしたい」といった調整が簡単です。

さらに、RayCast2D を使っているおかげで:

  • 地面が斜めでも、ちゃんとその位置に影が乗る
  • 穴や落下ゾーンでは地面が検出されず、影を消すことができる

など、ただの「Y座標固定の影」よりも表現力が高くなります。

改造案:影を「踏まれ判定」にも使う

応用として、影の上に乗ったら何かが起きるといったギミックも作れます。
例えば、ShadowBlob に「影の上を踏まれたかどうか」を検知する簡易処理を追加する例です:


## 影の上にプレイヤーが乗ったらコールバックを呼ぶ例
@export var enable_stepped_event: bool = false
signal stepped_on_shadow(body: Node)

func _physics_process(delta: float) -> void:
    if not _raycast or not _shadow_sprite:
        return

    _update_raycast_shape()
    _raycast.force_raycast_update()

    if _raycast.is_colliding():
        var hit_pos: Vector2 = _raycast.get_collision_point()
        if get_parent() is Node2D:
            var parent_pos: Vector2 = (get_parent() as Node2D).global_position
            global_position = hit_pos
            var height: float = max(parent_pos.y - hit_pos.y, 0.0)
            _update_shadow_scale_and_alpha(height)
            _shadow_sprite.visible = true

        if enable_stepped_event:
            var collider := _raycast.get_collider()
            if collider and collider != get_parent():
                emit_signal("stepped_on_shadow", collider)
    else:
        if hide_when_no_ground:
            _shadow_sprite.visible = false

このシグナル stepped_on_shadow をボスやスイッチに接続して、
「プレイヤーがボスの影を踏むとダメージ」みたいなギミックも作れますね。

こんな感じで、まずは「足元の丸影」をコンポーネントとして切り出しておくと、
後からどんどん表現やギミックに発展させやすくなります。継承にロジックを詰め込むより、小さなコンポーネントを組み合わせるスタイルで、Godot 4 ライフを快適にしていきましょう。