Godot 4で「動く床」や「ベルトコンベア」を作ろうとすると、ついプレイヤーや敵のスクリプト側に
「ベルトの上にいるときだけ横方向に速度を足す」みたいな条件分岐を書きがちですよね。

でもそのやり方だと…

  • プレイヤー、敵、動く箱など、対象ごとに同じような処理をコピペすることになる
  • 「このステージだけベルトの速度を変えたい」みたいな調整が、オブジェクト側のスクリプト修正になってしまう
  • ベルトの数が増えると、ノード階層もロジックもぐちゃっとなりがち

そこで今回は、「ベルトコンベアのロジックをベルト側に閉じ込める」コンポーネントを作って、
どんな物体でも上に乗せるだけで自動的に Velocity を加算してくれる仕組みを用意しましょう。

プレイヤーや敵は「自分でベルトを意識しない」。
ベルト側が「上に乗っているものに対して、一定方向の速度を加算する」だけ。
まさに 継承より合成(Composition) の考え方ですね。


【Godot 4】乗せるだけで動く床!「ConveyorBelt」コンポーネント

この ConveyorBelt コンポーネントは、ベルトの上に乗った物体に対して、
指定方向の「追加 Velocity」を自動で加えるためのスクリプトです。

  • ベルトの方向速度@export でインスペクタから調整可能
  • 上に乗っている間だけ、物体の velocitylinear_velocity を加算
  • プレイヤー・敵・動く箱など、物体側のスクリプトはほぼ無改造でOK

Godot 4 では 2D/3D 両方ありますが、今回はわかりやすく 2D専用 実装にします。
3D版にしたい場合の改造案も後半で触れます。


フルコード:ConveyorBelt.gd

ベルトそのものは Area2D として作り、その上に乗っているオブジェクトを検知して Velocity を加算します。


extends Area2D
class_name ConveyorBelt
"""
ConveyorBelt コンポーネント(2D用)

このノードの「上に乗っている」物体に対して、
指定方向のベルト速度を強制的に加算するコンポーネントです。

想定する対象:
- CharacterBody2D : velocity ベクトルを持っている
- RigidBody2D     : linear_velocity ベクトルを持っている
- Kinematic な独自実装 : velocity プロパティを持たせれば対応可

使い方:
- ConveyorBelt を Area2D としてシーンに置く
- CollisionShape2D でベルトの範囲を定義
- direction / speed / affect_airborne などをインスペクタで調整
"""

## === 設定パラメータ (インスペクタで編集可能) ===

@export_group("ベルト設定")

@export var direction: Vector2 = Vector2.RIGHT:
	set(value):
		# 常に正規化して扱う(方向ベクトル)
		direction = value.normalized() if value.length() != 0.0 else Vector2.ZERO

@export var speed: float = 80.0:
	set(value):
		speed = max(value, 0.0) # マイナスは禁止。向きは direction で決める

@export var affect_airborne: bool = false
## true の場合: 上に「重なっている」だけでベルト効果を与える
## false の場合: CharacterBody2D の is_on_floor() が true のときだけ加算(=ちゃんと乗っているとき)

@export var only_affect_bodies_with_group: StringName = &"" 
## 空文字列のとき: すべての対象を影響可能にする
## 何か文字列を入れた場合: そのグループに属するノードだけをベルト対象とする
## 例: "movable" を指定し、プレイヤー・敵・箱などに "movable" グループを付ける

@export_group("デバッグ表示")

@export var show_gizmo: bool = true
@export var gizmo_color: Color = Color(0.3, 0.8, 1.0, 0.5)

## === 内部状態 ===

# 今ベルト上にいる対象ノードのリスト
var _bodies: Array[Node] = []


func _ready() -> void:
	# Area2D のシグナルを接続(エディタで接続してもOKだが、コンポーネント化のためにコードで完結させる)
	connect("body_entered", Callable(self, "_on_body_entered"))
	connect("body_exited", Callable(self, "_on_body_exited"))


func _physics_process(delta: float) -> void:
	if direction == Vector2.ZERO or speed == 0.0:
		return

	var belt_velocity := direction * speed

	# 現在ベルト上にいる全ての対象に対して処理
	for body in _bodies:
		if not is_instance_valid(body):
			continue
		if not _can_affect_body(body):
			continue

		# CharacterBody2D の場合
		if body is CharacterBody2D:
			_apply_to_character_body(body, belt_velocity)

		# RigidBody2D の場合
		elif body is RigidBody2D:
			_apply_to_rigid_body(body, belt_velocity)

		# その他: velocity プロパティを持っているノードにも対応(簡易的なコンポーネント連携)
		elif "velocity" in body:
			_apply_to_generic_velocity(body, belt_velocity)


## === 対象判定 ===

func _on_body_entered(body: Node) -> void:
	if body in _bodies:
		return
	_bodies.append(body)


func _on_body_exited(body: Node) -> void:
	_bodies.erase(body)


func _can_affect_body(body: Node) -> bool:
	# グループ指定があれば、それに属しているものだけを対象にする
	if only_affect_bodies_with_group != &"":
		if not body.is_in_group(only_affect_bodies_with_group):
			return false
	return true


## === 各種ボディへの適用処理 ===

func _apply_to_character_body(body: CharacterBody2D, belt_velocity: Vector2) -> void:
	# 「ちゃんと乗っているときだけ効かせたい」場合は is_on_floor() を確認
	if not affect_airborne and not body.is_on_floor():
		return

	# body.velocity に対してベルトの速度を加算
	# ここでは「上書き」ではなく「加算」するのがポイント
	var v: Vector2 = body.velocity
	v += belt_velocity
	body.velocity = v


func _apply_to_rigid_body(body: RigidBody2D, belt_velocity: Vector2) -> void:
	# RigidBody2D は物理エンジンに任せるが、linear_velocity に加算することで
	# ベルト方向に流されているような挙動を作る
	var v: Vector2 = body.linear_velocity
	v += belt_velocity
	body.linear_velocity = v


func _apply_to_generic_velocity(body: Node, belt_velocity: Vector2) -> void:
	# velocity プロパティを持つ任意ノードに対応する簡易的な処理
	# 例: 独自の「MoveComponent」が velocity を公開している場合など
	var v: Variant = body.get("velocity")
	if typeof(v) == TYPE_VECTOR2:
		body.set("velocity", v + belt_velocity)


## === デバッグ描画 ===

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

	# ベルトの向きを矢印で表示(ローカル座標)
	var length := 32.0
	var from := Vector2.ZERO
	var to := direction.normalized() * length

	draw_line(from, to, gizmo_color, 2.0)

	# 矢印の頭
	var side := direction.normalized().orthogonal() * 6.0
	draw_line(to, to - direction.normalized() * 10.0 + side, gizmo_color, 2.0)
	draw_line(to, to - direction.normalized() * 10.0 - side, gizmo_color, 2.0)


func _process(delta: float) -> void:
	# エディタ上でパラメータを変えたときにも矢印が更新されるように
	if Engine.is_editor_hint():
		queue_redraw()

使い方の手順

手順①:ConveyorBelt.gd を用意する

上記コードをそのまま ConveyorBelt.gd として保存します。
プロジェクトのどこでも構いませんが、たとえば res://components/ConveyorBelt.gd のように
「components」フォルダを作ってまとめておくと、コンポーネント指向っぽくて管理しやすいですね。

手順②:ベルトコンベア用のシーンを作る

2D のステージに置く「ベルトコンベア」シーンを作りましょう。

ConveyorBeltPlatform (Node2D or StaticBody2D)
 ├── Sprite2D              # ベルトの見た目
 ├── CollisionShape2D      # (プレイヤーが乗る床の当たり判定)
 └── ConveyorBelt (Area2D) # ★今回のコンポーネント
      └── CollisionShape2D # ベルトの「影響範囲」(床と同じか、少しだけ厚めに)

ポイント:

  • ConveyorBelt ノードに ConveyorBelt.gd をアタッチ
  • ConveyorBeltCollisionShape2D は、プレイヤーが「ベルトの上にいる」と判定できるように
    床の CollisionShape2D とほぼ同じ範囲にしておくと扱いやすいです

インスペクタで以下を設定します:

  • direction: 右に流したいなら (1, 0)、左なら (-1, 0)、斜めも可
  • speed: ベルトの強さ(例: 80〜150 くらいで調整)
  • affect_airborne:
    • OFF: ちゃんと床に立っているときだけ効果(推奨)
    • ON: かすっているだけでも流される
  • only_affect_bodies_with_group: 例として "movable" などを指定すると、
    そのグループに属するノードだけがベルトの影響を受けます。

手順③:プレイヤーや敵に特別な改造はほぼ不要

プレイヤーが CharacterBody2D で、普通に velocity を使った移動をしているなら、
特別な対応はほぼ要りません。例えばこんなスクリプト:


extends CharacterBody2D

@export var move_speed: float = 200.0
@export var jump_speed: float = -400.0
@export var gravity: float = 900.0

func _physics_process(delta: float) -> void:
	var input_dir := Input.get_axis("ui_left", "ui_right")
	var v := velocity

	# 横移動
	v.x = move_speed * input_dir

	# 重力
	if not is_on_floor():
		v.y += gravity * delta

	# ジャンプ
	if is_on_floor() and Input.is_action_just_pressed("ui_accept"):
		v.y = jump_speed

	velocity = v
	move_and_slide()

このままで OK です。
ConveyorBelt コンポーネントが velocity に対して「加算」してくれるので、
プレイヤー側は「ベルトの存在を知らない」まま動かせます。

もしグループを使って絞り込みたい場合は、プレイヤーに movable グループを付けておきましょう。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── (他のコンポーネントいろいろ)

インスペクタの「Node」タブ → Groups → movable を追加。

手順④:敵や動く箱にもそのまま使い回し

敵や動く箱も同様に、「velocity を持っている」か「RigidBody2D」 であれば、
ConveyorBelt の上に置くだけでベルトの影響を受けます。

例:動く箱(RigidBody2D)のシーン構成:

Box (RigidBody2D)
 ├── Sprite2D
 └── CollisionShape2D

これをベルトの上に置くだけで、linear_velocity にベルト速度が加算され、
箱がベルトに運ばれていきます。


メリットと応用

この ConveyorBelt コンポーネントを使うことで、次のようなメリットがあります。

  • シーン構造がスッキリ:プレイヤーや敵のスクリプトから「ベルト判定」が消える
  • レベルデザインが楽:ベルトのスピードや向きをインスペクタからポチポチ変えるだけ
  • 再利用性が高い:どのステージにも同じコンポーネントを置くだけで動作
  • バグの局所化:ベルトの挙動にバグがあっても、修正は ConveyorBelt.gd だけで済む

「プレイヤー」「敵」「ギミック」などのロジックを各ノードにベタ書きすると、
あとから「このステージだけベルトの挙動を変えたい」となったときに地獄を見ます。
コンポーネントとして分離しておけば、ベルト側だけを差し替えることができるので安心ですね。

改造案:ベルト上のオブジェクトを「一定速度に揃える」モード

今の実装は「Velocity を加算する」方式なので、プレイヤーが自力で逆走すると
ベルトに逆らって歩くことができます。
もし「ベルトに乗ったら、強制的に一定速度で流される」ようにしたい場合は、
以下のような関数を追加して、_apply_to_character_body の代わりに使うこともできます。


func _apply_to_character_body_lock_speed(body: CharacterBody2D, belt_velocity: Vector2) -> void:
	# ちゃんと床に立っているときだけ
	if not affect_airborne and not body.is_on_floor():
		return

	var v: Vector2 = body.velocity

	# ベルトの軸方向成分だけを強制的に上書きするイメージ
	var axis := direction.normalized()
	var perpendicular := Vector2(-axis.y, axis.x)

	# 現在の速度をベルト方向成分と垂直成分に分解
	var along := axis * v.dot(axis)
	var side := perpendicular * v.dot(perpendicular)

	# ベルト方向成分を「ベルトの速度」で上書き
	along = belt_velocity

	# 垂直成分(ジャンプなど)はそのまま残す
	body.velocity = along + side

このように、コンポーネントの中に「モード違いの適用関数」を用意しておけば、
ステージに応じて「加算型ベルト」「ロック型ベルト」などを簡単に切り替えられます。

コンポーネント指向でロジックを分離しておくと、こういった挙動の差分も
「ベルト側だけを差し替える」だけで済むので、とても気持ちいいですね。