Godot 4で「動く床」や「ベルトコンベア」を作ろうとすると、ついプレイヤーや敵のスクリプト側に
「ベルトの上にいるときだけ横方向に速度を足す」みたいな条件分岐を書きがちですよね。
でもそのやり方だと…
- プレイヤー、敵、動く箱など、対象ごとに同じような処理をコピペすることになる
- 「このステージだけベルトの速度を変えたい」みたいな調整が、オブジェクト側のスクリプト修正になってしまう
- ベルトの数が増えると、ノード階層もロジックもぐちゃっとなりがち
そこで今回は、「ベルトコンベアのロジックをベルト側に閉じ込める」コンポーネントを作って、
どんな物体でも上に乗せるだけで自動的に Velocity を加算してくれる仕組みを用意しましょう。
プレイヤーや敵は「自分でベルトを意識しない」。
ベルト側が「上に乗っているものに対して、一定方向の速度を加算する」だけ。
まさに 継承より合成(Composition) の考え方ですね。
【Godot 4】乗せるだけで動く床!「ConveyorBelt」コンポーネント
この ConveyorBelt コンポーネントは、ベルトの上に乗った物体に対して、
指定方向の「追加 Velocity」を自動で加えるためのスクリプトです。
- ベルトの方向と速度を
@exportでインスペクタから調整可能 - 上に乗っている間だけ、物体の
velocityやlinear_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 をアタッチConveyorBeltのCollisionShape2Dは、プレイヤーが「ベルトの上にいる」と判定できるように
床の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
このように、コンポーネントの中に「モード違いの適用関数」を用意しておけば、
ステージに応じて「加算型ベルト」「ロック型ベルト」などを簡単に切り替えられます。
コンポーネント指向でロジックを分離しておくと、こういった挙動の差分も
「ベルト側だけを差し替える」だけで済むので、とても気持ちいいですね。
