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)
Playerシーンを開く- 子ノードとして
Node2Dを追加し、名前をShadowCasterに変更 - この子ノードに上記の
ShadowCaster.gdをアタッチ - インスペクタで
shadow_textureに楕円画像を指定base_scaleをキャラの大きさに合わせて調整(例:(1.2, 0.4))ground_offsetを(0, 16)など、足元の位置に合わせて調整height_modeをFROM_PARENT_METHODに設定
ShadowCaster ノード自体は、親のローカル座標で「足元」あたりに配置しておくと分かりやすいです。
(ただしスクリプト側で ground_offset を使っているので、原点に置いてオフセットで調整するのもアリです。)
手順③:親(プレイヤー)に「高さを返すメソッド」を実装
次に、プレイヤー側で「今どれだけジャンプしているか(高さ)」を返すメソッドを用意します。
例として、Player が CharacterBody2D で、vertical_velocity や on_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 をベースにガンガン改造してみてください。
