Godot 4 でキャラクターの足元に影を置きたいとき、どうしていますか?
多くのプロジェクトでは、

  • プレイヤーシーンに Sprite2D を追加して「影」として使う
  • 敵ごとに影のスプライトを複製して、ポジションやスケールを個別に調整する

……みたいな「コピペ&調整地獄」にハマりがちです。
さらに、ジャンプするキャラの場合は「高さに応じて影を小さくする」「着地で元に戻す」といった処理を、各キャラのスクリプトにベタ書きしがちですよね。

そこで今回は、どんなキャラにもポン付けできる「足元の影コンポーネント」を用意して、

  • 親の足元に自動で楕円影を描画
  • ジャンプ高さに応じて影の大きさを自動で縮小
  • コードはコンポーネント側に隔離して、プレイヤーや敵のスクリプトをスリム化

を実現していきましょう。
継承で「影付きプレイヤー」「影付き敵」を増やしていくより、シンプルな親ノード + ShadowCaster コンポーネントの合成で組み立てた方が、後からの拡張もしやすいですね。

【Godot 4】ジャンプ高さで自動調整!「ShadowCaster」コンポーネント

今回の ShadowCaster は、

  • 親ノードの「地面上の足元位置」を基準に影を配置
  • 「現在の高さ」を親から受け取り、スケールを補正
  • 影用のテクスチャをインスペクタから差し替え可能

というコンポーネント指向のスクリプトです。
「高さ」の取得方法はプロジェクトごとに違うので、親から高さを教えてもらう方式にしてあります。
(例:get_shadow_height() を実装してもらう)


フルコード(GDScript / Godot 4)


extends Node2D
class_name ShadowCaster
## 親の足元に楕円の影を描画し、高さに応じてスケールを変えるコンポーネント。
##
## 想定する親ノード:
## - CharacterBody2D / RigidBody2D / Node2D など
## - 「高さ」を返すメソッドを実装しているとより便利:
##      func get_shadow_height() -> float:
##          return current_height
##
## もしくは、親の position.y の変化をそのまま高さとして扱う簡易モードもあります。

@export_category("Shadow Visual")
## 影として使うテクスチャ。楕円形のPNGなどを指定するとそれっぽくなります。
@export var shadow_texture: Texture2D

## 影の基本スケール(高さ0のとき)。キャラの大きさに合わせて調整しましょう。
@export var base_scale: Vector2 = Vector2(1.0, 0.5)

## 影の色。アルファで濃さを調整します。
@export var shadow_color: Color = Color(0, 0, 0, 0.5)

@export_category("Height & Scaling")
## 高さ0のときの「地面からの基準Yオフセット」。
## 例: キャラの足元が親の原点より少し下にある場合など。
@export var ground_offset: Vector2 = Vector2(0, 0)

## どの高さまで影をスケールさせるか。
## これを超える高さでは min_scale まで縮小されたままになります。
@export var max_height: float = 100.0

## 高さが max_height のときの影スケール倍率。
## 例: Vector2(0.3, 0.3) にすると、かなり小さくなる。
@export var min_scale: Vector2 = Vector2(0.3, 0.3)

## 高さをどこから取得するかのモード。
enum HeightMode {
    FROM_PARENT_METHOD,  ## 親の get_shadow_height() から取得
    FROM_PARENT_Y_DELTA  ## 親のY座標の変化量を高さとして扱う(簡易モード)
}

@export var height_mode: HeightMode = HeightMode.FROM_PARENT_METHOD

## FROM_PARENT_Y_DELTA モードで使用する、基準Y座標。
var _base_parent_y: float = 0.0

## 内部で使用する Sprite2D(影本体)
var _shadow_sprite: Sprite2D

func _ready() -> void:
    # 影用 Sprite2D を自動生成して子に追加します。
    _shadow_sprite = Sprite2D.new()
    add_child(_shadow_sprite)

    # 初期設定
    _shadow_sprite.texture = shadow_texture
    _shadow_sprite.modulate = shadow_color
    _shadow_sprite.centered = true
    _shadow_sprite.position = ground_offset
    _shadow_sprite.scale = base_scale

    # 親の初期Yを記録(FROM_PARENT_Y_DELTA モード用)
    if height_mode == HeightMode.FROM_PARENT_Y_DELTA and get_parent() != null:
        _base_parent_y = get_parent().global_position.y

func _process(delta: float) -> void:
    if get_parent() == null:
        return

    # 高さを取得
    var h := _get_current_height()

    # 高さに応じてスケールを補間
    var t := clamp(h / max_height, 0.0, 1.0)
    # t=0 -> base_scale, t=1 -> min_scale となるように線形補間
    var new_scale := base_scale.lerp(min_scale, t)
    _shadow_sprite.scale = new_scale

    # 影の位置は「親の足元(ground_offset)」に固定
    # ShadowCaster 自体を親のローカル座標で足元に置いておけば OK。
    # ここではあえて何もしませんが、必要ならここで位置補正も可能です。
    _shadow_sprite.position = ground_offset

func _get_current_height() -> float:
    ## 現在の高さを取得する内部メソッド。
    ## height_mode によって取得方法を変えています。
    var parent := get_parent()
    if parent == null:
        return 0.0

    match height_mode:
        HeightMode.FROM_PARENT_METHOD:
            # 親に get_shadow_height() が実装されていることを期待
            if parent.has_method("get_shadow_height"):
                var h = parent.call("get_shadow_height")
                if typeof(h) == TYPE_FLOAT or typeof(h) == TYPE_INT:
                    return float(h)
            # 実装されていない場合はログを出して 0 を返す
            push_warning("Parent has no get_shadow_height() method. Height will be 0.")
            return 0.0

        HeightMode.FROM_PARENT_Y_DELTA:
            # 親のY座標が _base_parent_y より上に行くほど「高さが増える」とみなす簡易モード。
            var dy := _base_parent_y - parent.global_position.y
            return max(dy, 0.0)

        _:
            return 0.0

func set_shadow_enabled(enabled: bool) -> void:
    ## 外部から影のオン/オフを切り替えたい場合に使うユーティリティ。
    if is_instance_valid(_shadow_sprite):
        _shadow_sprite.visible = enabled

func set_shadow_texture(tex: Texture2D) -> void:
    ## ランタイムで影のテクスチャを差し替えたい場合に使用。
    shadow_texture = tex
    if is_instance_valid(_shadow_sprite):
        _shadow_sprite.texture = tex

使い方の手順

今回は 2D のプレイヤーを例にしますが、敵や動く床にも同じコンポーネントをそのまま使えます。

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

  • 画像編集ツールで、黒〜グレーの楕円 + 透過背景のPNGを用意します。
  • インポート設定で Filter = On / Repeat = Disabled など、通常のSprite用設定でOKです。

手順②:プレイヤーシーンに ShadowCaster を追加

例として、こんなプレイヤーシーンを想定します:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── ShadowCaster (Node2D)
  1. Player シーンを開く
  2. 子ノードとして Node2D を追加し、名前を ShadowCaster に変更
  3. この子ノードに上記の ShadowCaster.gd をアタッチ
  4. インスペクタで
    • shadow_texture に楕円画像を指定
    • base_scale をキャラの大きさに合わせて調整(例: (1.2, 0.4)
    • ground_offset(0, 16) など、足元の位置に合わせて調整
    • height_modeFROM_PARENT_METHOD に設定

ShadowCaster ノード自体は、親のローカル座標で「足元」あたりに配置しておくと分かりやすいです。
(ただしスクリプト側で ground_offset を使っているので、原点に置いてオフセットで調整するのもアリです。)

手順③:親(プレイヤー)に「高さを返すメソッド」を実装

次に、プレイヤー側で「今どれだけジャンプしているか(高さ)」を返すメソッドを用意します。
例として、PlayerCharacterBody2D で、vertical_velocityon_floor を使ったシンプルなジャンプをしているケースを考えます。


# Player.gd (CharacterBody2D にアタッチされている想定)
extends CharacterBody2D

@export var gravity: float = 1200.0
@export var jump_speed: float = -450.0

var _height: float = 0.0  # 地面からの現在の高さ

func _physics_process(delta: float) -> void:
    # 簡易的なジャンプ処理の例
    if is_on_floor():
        # 地面にいるときにジャンプボタンで上方向速度を与える
        if Input.is_action_just_pressed("ui_accept"):
            velocity.y = jump_speed
    else:
        # 空中では重力を加える
        velocity.y += gravity * delta

    # 実際の移動
    move_and_slide()

    # 高さを更新
    _update_height(delta)

func _update_height(delta: float) -> void:
    # ここでは「地面のY位置」を仮に 0 として、高さを疑似的に計算します。
    # 実際のゲームでは、タイルマップやフロアのY座標から算出してもよいです。
    # 例として、プレイヤーの global_position.y が小さいほど高くジャンプしているとみなす。
    # ここでは簡単に「速度から積分した高さ」を使います。

    # 地面にいるときは高さ0にリセット
    if is_on_floor():
        _height = 0.0
    else:
        # 上向きがマイナスなので、-velocity.y を使うと「上に行くほど正の値」になる
        # これを少しマイルドにするために 0.02 を掛けてスケーリング
        _height = max(_height + (-velocity.y * 0.02 * delta), 0.0)

func get_shadow_height() -> float:
    # ShadowCaster から呼ばれる高さ取得用メソッド
    return _height

このように get_shadow_height() を実装しておけば、ShadowCaster コンポーネントが勝手に高さを読んで影のスケールを調整してくれます。

手順④:敵や動く床にもそのまま再利用

敵キャラやジャンプする足場にも同じやり方で適用できます。

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

敵側で get_shadow_height() を実装しておけば、まったく同じ ShadowCaster をそのままアタッチするだけでOKです。
「影付き敵クラス」を継承で増やす必要はなく、ベースの Enemy に ShadowCaster をポン付けするだけで済みます。


メリットと応用

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

  • 影のロジックが1か所に集約されるので、調整が楽
    • 「影をもっと小さくしたい」「色を変えたい」などの変更が、全キャラに一括で反映
  • プレイヤーや敵のスクリプトがスリムになる
    • 各キャラは「高さを返すだけ」でOK。影のスケール計算はコンポーネントに任せる。
  • シーン構造が浅く・シンプルに保てる
    • 「影付きプレイヤー」「影付き敵」などの派生シーンを作らず、共通コンポーネントをアタッチするだけ。
  • レベルデザイン時の再利用性が高い
    • 敵を複製するときに影も一緒についてくるので、配置の手戻りが減る。

継承ベースで「影付きキャラ階層」を作ると、後から仕様が変わった時にツリー全体を修正する羽目になりがちです。
コンポーネントとして切り出しておけば、影の仕様変更もこの1ファイルだけで完結するので、保守性が段違いですね。

改造案:高さに応じて影の透明度も変える

「高くジャンプしたら影を薄くしたい」という場合は、_process() に少しコードを足すだけで実現できます。


func _process(delta: float) -> void:
    if get_parent() == null:
        return

    var h := _get_current_height()
    var t := clamp(h / max_height, 0.0, 1.0)

    # スケール補間
    var new_scale := base_scale.lerp(min_scale, t)
    _shadow_sprite.scale = new_scale

    # 透明度も補間 (高さ0で元のアルファ、高さmaxで半分のアルファに)
    var base_alpha := shadow_color.a
    var new_alpha := lerp(base_alpha, base_alpha * 0.3, t)
    var c := shadow_color
    c.a = new_alpha
    _shadow_sprite.modulate = c

    _shadow_sprite.position = ground_offset

こうやって少しずつ機能を足していけるのも、コンポーネントとして独立しているからこそですね。
影まわりの演出をいじりたくなったら、ぜひ ShadowCaster をベースにガンガン改造してみてください。