Godot 4で背景のパララックスをやろうとすると、まず思い浮かぶのは ParallaxBackground と ParallaxLayer ですよね。でも実際に触ってみると:
- シーン階層に
ParallaxBackground > ParallaxLayer > Spriteを毎回組まないといけない - 既存の
Sprite2DやTextureRectを「パララックス対応」にしたいだけなのに、シーン構成を組み替える必要がある - カメラの追従やズームとの連携を自分でちょっとカスタムしたいとき、Parallax系ノードの挙動に縛られる
…と、「ちょっと背景をスクロールさせたいだけ」なのに、意外と構造が重くなりがちです。
そこでこの記事では、任意の Sprite2D や TextureRect にペタッと貼るだけで「カメラに対して少し遅れて動く」擬似パララックスを実現するコンポーネント ParallaxLayerMover を紹介します。
ノード階層をいじらずに、「カメラの動きに反応する背景コンポーネント」をアタッチするだけ、という 継承より合成 なアプローチでいきましょう。
【Godot 4】どのノードにも貼れるお手軽パララックス!「ParallaxLayerMover」コンポーネント
以下がフルコードです。Node2D / Sprite2D / TextureRect など「2D座標を持つノード」にアタッチして使います。
## ParallaxLayerMover.gd
## 任意の2Dノードを「カメラに対して少し遅れて動く背景」に変えるコンポーネント
extends Node
class_name ParallaxLayerMover
## ─────────────────────────────────────────────
## エクスポート変数(インスペクタで設定可能なパラメータ)
## ─────────────────────────────────────────────
@export var camera_path: NodePath:
## このコンポーネントが参照するカメラ
## - 空のままなら、自動で現在のSceneTreeから最初に見つかったCamera2Dを使用
## - 特定のCamera2Dだけを参照したい場合は、シーン内のCamera2Dへのパスを指定
get:
return camera_path
set(value):
camera_path = value
# シーン上で変更された場合にも再解決できるようにする
_resolve_camera()
@export_range(0.0, 1.0, 0.01)
var parallax_factor: float = 0.5:
## パララックスの強さ
## 0.0 = カメラの動きに影響されない(固定背景)
## 1.0 = カメラと同じだけ動く(パララックス効果なし)
## 0.2〜0.7 くらいが「遠景」っぽくてオススメ
get:
return parallax_factor
set(value):
parallax_factor = clamp(value, 0.0, 1.0)
@export var affect_x: bool = true:
## X方向のパララックスを有効にするか
## 横スクロールゲームなら true 推奨
get:
return affect_x
set(value):
affect_x = value
@export var affect_y: bool = false:
## Y方向のパララックスを有効にするか
## 上下にも動くゲーム(トップダウンなど)の場合は true にすると奥行き感が増す
get:
return affect_y
set(value):
affect_y = value
@export var use_global_position: bool = true:
## グローバル座標ベースで制御するかどうか
## true : グローバル座標でカメラとの差分を取る(大抵はこちらでOK)
## false : ローカル座標でカメラとの差分を取る(特殊な階層構造で使いたい場合)
get:
return use_global_position
set(value):
use_global_position = value
@export var base_position_mode_global: bool = true:
## 基準位置(カメラが origin のときの位置)をグローバルで記録するかローカルで記録するか
## - ほとんどの場合 true で問題ありません
get:
return base_position_mode_global
set(value):
base_position_mode_global = value
@export var debug_draw_gizmo: bool = false:
## デバッグ用。エディタ上で基準位置を簡易表示したいときに使う
get:
return debug_draw_gizmo
set(value):
debug_draw_gizmo = value
queue_redraw()
## ─────────────────────────────────────────────
## 内部状態
## ─────────────────────────────────────────────
var _camera: Camera2D = null
## パララックスの「基準位置」
## - カメラが (0, 0) のとき、このノードはどこにいるべきか
var _base_position_global: Vector2
var _base_position_local: Vector2
## 現在のカメラ位置を記録(差分計算用)
var _initial_camera_position: Vector2
## 親ノード(パララックスさせたい対象)
var _target_2d: Node2D = null
func _ready() -> void:
## このコンポーネントがアタッチされたノードを取得
_target_2d = _find_target_2d()
if _target_2d == null:
push_warning("ParallaxLayerMover: 親に Node2D 系のノードが見つかりません。Sprite2D や Node2D にアタッチしてください。")
set_process(false)
return
_resolve_camera()
## カメラが見つからない場合は処理を止める(警告のみ)
if _camera == null:
push_warning("ParallaxLayerMover: Camera2D が見つかりません。後から camera_path を設定するか、シーンに Camera2D を追加してください。")
set_process(false)
return
## 基準位置とカメラの初期位置を記録
_store_base_positions()
_initial_camera_position = _get_camera_position()
set_process(true)
func _process(_delta: float) -> void:
if _camera == null or _target_2d == null:
return
var cam_pos := _get_camera_position()
var cam_offset := cam_pos - _initial_camera_position
## カメラのオフセットに対して、1 - parallax_factor 倍だけ「遅れて」動かす
## parallax_factor が 1.0 のとき → 遅れゼロ(カメラと同じ)
## parallax_factor が 0.0 のとき → 完全固定(背景)
var lag_factor := 1.0 - parallax_factor
var parallax_offset := cam_offset * lag_factor
## X/Y の有効フラグを適用
if not affect_x:
parallax_offset.x = 0.0
if not affect_y:
parallax_offset.y = 0.0
if use_global_position:
var base := base_position_mode_global \
? _base_position_global \
: _target_2d.get_global_transform_with_canvas().origin
_target_2d.global_position = base + parallax_offset
else:
var base_local := base_position_mode_global \
? _target_2d.to_local(_base_position_global) \
: _base_position_local
_target_2d.position = base_local + parallax_offset
func _notification(what: int) -> void:
## エディタ上での簡易ギズモ描画
if what == NOTIFICATION_EDITOR_DRAW and debug_draw_gizmo:
if _target_2d:
var draw_pos := _target_2d.position
var color := Color(0.2, 0.8, 1.0, 0.8)
draw_circle(draw_pos, 4.0, color)
draw_line(draw_pos + Vector2(-8, 0), draw_pos + Vector2(8, 0), color, 1.5)
draw_line(draw_pos + Vector2(0, -8), draw_pos + Vector2(0, 8), color, 1.5)
## ─────────────────────────────────────────────
## ヘルパー関数
## ─────────────────────────────────────────────
func _find_target_2d() -> Node2D:
## 通常は「このコンポーネントが付いているノード自身」を対象とする
if owner is Node2D:
return owner as Node2D
## それ以外の場合、親方向に Node2D を探す(柔軟性のため)
var p := get_parent()
while p:
if p is Node2D:
return p as Node2D
p = p.get_parent()
return null
func _resolve_camera() -> void:
_camera = null
## camera_path が設定されていれば、それを優先して取得
if camera_path != NodePath(""):
var node := get_node_or_null(camera_path)
if node and node is Camera2D:
_camera = node
return
else:
push_warning("ParallaxLayerMover: camera_path に指定されたノードが Camera2D ではありません。自動探索にフォールバックします。")
## SceneTree から最初に見つかった Camera2D を使用する
var tree := get_tree()
if tree:
for cam in tree.get_nodes_in_group("cameras"):
if cam is Camera2D:
_camera = cam
return
## グループ未使用の場合、素朴に全ノードから探す
for node in tree.get_nodes_in_group(""):
if node is Camera2D:
_camera = node
return
func _store_base_positions() -> void:
## 「カメラが初期位置のとき、この背景はどこにあるべきか」を保存
if _target_2d == null:
return
if base_position_mode_global:
_base_position_global = _target_2d.global_position
_base_position_local = _target_2d.position
else:
_base_position_local = _target_2d.position
_base_position_global = _target_2d.global_position
func _get_camera_position() -> Vector2:
if _camera == null:
return Vector2.ZERO
return _camera.global_position
使い方の手順
ここでは横スクロールの2Dアクションを例に、「遠景の山」と「中景の雲」をパララックスさせるケースで説明します。
手順①:コンポーネントスクリプトを用意する
- 上記の
ParallaxLayerMover.gdを新規スクリプトとして保存します(例:res://components/ParallaxLayerMover.gd)。 - スクリプトに
class_name ParallaxLayerMoverが含まれているので、Godotエディタから直接「スクリプトクラス」として選択できます。
手順②:背景ノードにコンポーネントをアタッチする
例えば、こんなシーン構成にしてみましょう:
MainScene (Node2D)
├── Camera2D
├── Player (CharacterBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
├── FarMountains (Sprite2D)
│ └── ParallaxLayerMover (Node)
└── Clouds (Sprite2D)
└── ParallaxLayerMover (Node)
FarMountains:遠くの山の画像を貼ったSprite2DClouds:雲の画像を貼ったSprite2D- それぞれの子に
ParallaxLayerMoverノードを追加し、スクリプトをParallaxLayerMover.gdに設定します。
この構成であれば、コンポーネントは「親の Sprite2D」を自動でターゲットにしてくれます。
手順③:インスペクタでパラメータを調整する
FarMountains > ParallaxLayerMover の設定例:
camera_path:空のままでOK(シーン内のCamera2Dを自動検出)parallax_factor:0.2(かなり遠景っぽく)affect_x:trueaffect_y:false(横スクロールのみならオフでOK)
Clouds > ParallaxLayerMover の設定例:
parallax_factor:0.6(中景っぽく)affect_x:trueaffect_y:false(必要ならtrueにして上下にも揺らす)
プレイヤーを追従する Camera2D が動くと、FarMountains と Clouds がそれぞれ少し遅れて動き、奥行き感のある背景スクロールになります。
手順④:別シーンでもコピペで再利用する
このコンポーネントの良いところは、背景の Sprite2D さえあれば、そこにペタッと貼るだけでパララックス化できることです。
例えば、別シーンで「動く床」的な演出をしたい場合:
Stage2 (Node2D)
├── Camera2D
├── Player (CharacterBody2D)
├── MovingPlatform (Node2D)
│ ├── Sprite2D
│ └── CollisionShape2D
└── BackgroundCity (TextureRect)
└── ParallaxLayerMover (Node)
BackgroundCity(TextureRect)にParallaxLayerMoverを付ければ、UIレイヤーに配置した背景でもパララックスを簡単に適用できます。MovingPlatform自体にはこのコンポーネントは不要ですが、もし「カメラの動きに対して少し遅れて動くギミック」を作りたければ、同様にアタッチするだけです。
メリットと応用
この ParallaxLayerMover コンポーネントを使うメリットを整理すると:
- シーン構造を汚さない:既存の
Sprite2D/TextureRectにノード1つ足すだけでOK。ParallaxBackgroundのために階層を組み替える必要がありません。 - コンポーネントとして再利用しやすい:プレイヤー、敵、背景、UIなど「カメラに対して相対的に動かしたいもの」なら何にでも貼れる、汎用的な合成パーツです。
- カメラの切り替えにも対応しやすい:
camera_pathで特定のカメラを指定できるので、ステージごとに別カメラを使うような構成でも柔軟に対応できます。 - スクリプトが1か所にまとまる:パララックスロジックを各シーンにコピペするのではなく、1つのコンポーネントに閉じ込めておけるので保守が楽です。
「継承で ParallaxSprite クラスを作る」のではなく、「既存の Sprite2D に ParallaxLayerMover を合成する」という発想にすると、後からの拡張や組み合わせがかなり楽になりますね。
改造案:無限ループスクロールと組み合わせる
さらに発展させて、「一定距離動いたら画像をループさせる」処理を別コンポーネントとして作り、ParallaxLayerMover と組み合わせるのもアリです。例えば:
## InfiniteScroll2D.gd
## 一定幅ごとにSprite2Dをループさせる簡易コンポーネント
extends Node
class_name InfiniteScroll2D
@export var scroll_width: float = 1024.0
@export var use_global_position: bool = true
var _target_2d: Node2D
var _start_pos: Vector2
func _ready() -> void:
_target_2d = owner as Node2D
if _target_2d == null:
push_warning("InfiniteScroll2D: 親が Node2D ではありません。")
set_process(false)
return
_start_pos = use_global_position \
? _target_2d.global_position \
: _target_2d.position
set_process(true)
func _process(_delta: float) -> void:
if use_global_position:
var offset := _target_2d.global_position.x - _start_pos.x
if offset > scroll_width:
_target_2d.global_position.x -= scroll_width
elif offset < -scroll_width:
_target_2d.global_position.x += scroll_width
else:
var offset := _target_2d.position.x - _start_pos.x
if offset > scroll_width:
_target_2d.position.x -= scroll_width
elif offset < -scroll_width:
_target_2d.position.x += scroll_width
これを Sprite2D に ParallaxLayerMover と併用 すれば、「カメラに合わせて遅れて動く & 一生続く背景スクロール」を、継承なし・コンポーネントの合成だけで実現できます。
こういう小さなコンポーネントを積み重ねていくと、「あのシーンでやったアレをまた使いたい」がすごく楽になるので、ぜひ自分用のコンポーネントライブラリに加えてみてください。




