【Godot 4】ParallaxLayerMover (背景スクロール) コンポーネントの作り方

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

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

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

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

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

Godot 4で背景のパララックスをやろうとすると、まず思い浮かぶのは ParallaxBackgroundParallaxLayer ですよね。でも実際に触ってみると:

  • シーン階層に ParallaxBackground > ParallaxLayer > Sprite を毎回組まないといけない
  • 既存の Sprite2DTextureRect を「パララックス対応」にしたいだけなのに、シーン構成を組み替える必要がある
  • カメラの追従やズームとの連携を自分でちょっとカスタムしたいとき、Parallax系ノードの挙動に縛られる

…と、「ちょっと背景をスクロールさせたいだけ」なのに、意外と構造が重くなりがちです。

そこでこの記事では、任意の Sprite2DTextureRect にペタッと貼るだけで「カメラに対して少し遅れて動く」擬似パララックスを実現するコンポーネント 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アクションを例に、「遠景の山」と「中景の雲」をパララックスさせるケースで説明します。

手順①:コンポーネントスクリプトを用意する

  1. 上記の ParallaxLayerMover.gd を新規スクリプトとして保存します(例:res://components/ParallaxLayerMover.gd)。
  2. スクリプトに class_name ParallaxLayerMover が含まれているので、Godotエディタから直接「スクリプトクラス」として選択できます。

手順②:背景ノードにコンポーネントをアタッチする

例えば、こんなシーン構成にしてみましょう:

MainScene (Node2D)
 ├── Camera2D
 ├── Player (CharacterBody2D)
 │    ├── Sprite2D
 │    └── CollisionShape2D
 ├── FarMountains (Sprite2D)
 │    └── ParallaxLayerMover (Node)
 └── Clouds (Sprite2D)
      └── ParallaxLayerMover (Node)
  • FarMountains:遠くの山の画像を貼った Sprite2D
  • Clouds:雲の画像を貼った Sprite2D
  • それぞれの子に ParallaxLayerMover ノードを追加し、スクリプトを ParallaxLayerMover.gd に設定します。

この構成であれば、コンポーネントは「親の Sprite2D」を自動でターゲットにしてくれます。

手順③:インスペクタでパラメータを調整する

FarMountains > ParallaxLayerMover の設定例:

  • camera_path:空のままでOK(シーン内の Camera2D を自動検出)
  • parallax_factor0.2(かなり遠景っぽく)
  • affect_xtrue
  • affect_yfalse(横スクロールのみならオフでOK)

Clouds > ParallaxLayerMover の設定例:

  • parallax_factor0.6(中景っぽく)
  • affect_xtrue
  • affect_yfalse(必要ならtrueにして上下にも揺らす)

プレイヤーを追従する Camera2D が動くと、FarMountainsClouds がそれぞれ少し遅れて動き、奥行き感のある背景スクロールになります。

手順④:別シーンでもコピペで再利用する

このコンポーネントの良いところは、背景の Sprite2D さえあれば、そこにペタッと貼るだけでパララックス化できることです。

例えば、別シーンで「動く床」的な演出をしたい場合:

Stage2 (Node2D)
 ├── Camera2D
 ├── Player (CharacterBody2D)
 ├── MovingPlatform (Node2D)
 │    ├── Sprite2D
 │    └── CollisionShape2D
 └── BackgroundCity (TextureRect)
      └── ParallaxLayerMover (Node)
  • BackgroundCityTextureRect)に 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

これを Sprite2DParallaxLayerMover と併用 すれば、「カメラに合わせて遅れて動く & 一生続く背景スクロール」を、継承なし・コンポーネントの合成だけで実現できます。

こういう小さなコンポーネントを積み重ねていくと、「あのシーンでやったアレをまた使いたい」がすごく楽になるので、ぜひ自分用のコンポーネントライブラリに加えてみてください。

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

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

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

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

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!