Godot 4 で「動く床」「落ちる床」を作るとき、ありがちなのがこういうパターンですね。

  • TileMap で床を並べて、特定のタイルだけスクリプトで制御しようとしてカオスになる
  • 落ちる床専用のシーンを作り、さらにそこからバリエーションごとに継承してシーンツリーがどんどん増える
  • 床ごとにタイマーやアニメーションプレイヤーを直付けして、インスペクタがパラメータだらけになる

結果として:

  • 「この床はどのスクリプトで制御してるんだっけ?」と迷子になる
  • 床の挙動をちょっと変えたいだけなのに、複数シーンを修正しないといけない
  • プレイヤーや敵のスクリプトが「床の判定」まで抱え込んでどんどん肥大化する

そこで今回は、「落ちる床」という挙動を 1つのコンポーネント に切り出して、どんな床ノードにもポン付けできるようにしてみましょう。
コンポーネント名は FallingPlatform。乗ってから 0.5 秒後に揺れ始め、1 秒後に物理挙動で落下する床を実装します。

【Godot 4】乗ったら揺れて落ちる床をコンポーネント化!「FallingPlatform」コンポーネント

このコンポーネントは「床そのもの」ではなく、「床に取り付ける動作モジュール」です。
StaticBody2D / CharacterBody2D / RigidBody2D など、いろんな種類の床ノードにアタッチ可能で、既存シーンにあとから足すのも簡単です。


FallingPlatform コンポーネントのフルコード


extends Node
class_name FallingPlatform
## 落ちる床コンポーネント
## 親ノード(床)にアタッチして使う。
##
## 想定する親ノード:
## - StaticBody2D (一番おすすめ)
## - CharacterBody2D
## - Node2D + CollisionShape2D など
##
## 親ノードは「最初は静止した床」で、
## 一定時間後に RigidBody2D に差し替えて落下させる方式です。

@export_range(0.0, 5.0, 0.1)
var shake_delay: float = 0.5:
	## プレイヤーが乗ってから「揺れ始めるまで」の時間(秒)
	set(value):
		shake_delay = max(value, 0.0)

@export_range(0.0, 5.0, 0.1)
var fall_delay: float = 1.0:
	## プレイヤーが乗ってから「落下開始まで」の時間(秒)
	## shake_delay 以上であることを推奨
	set(value):
		fall_delay = max(value, 0.0)

@export_range(0.0, 50.0, 0.1)
var shake_amplitude: float = 4.0
## 揺れの振れ幅(ピクセル)。0 にすると揺れなし。

@export_range(0.0, 50.0, 0.1)
var shake_frequency: float = 15.0
## 揺れの速さ(Hz)。値が大きいほどブルブルする。

@export
var one_time_use: bool = true
## true の場合、一度落下したら再利用しない。
## false の場合、一定時間後に元の位置に戻すなどの拡張がやりやすい。

@export_range(0.0, 10.0, 0.1)
var auto_free_after_fall: float = 0.0
## 落下後、自動で削除するまでの時間(秒)
## 0 の場合は削除しない。

@export_group("Detection")
@export
var use_body_enter_signal: bool = true
## true: 親に Area2D / CollisionObject2D があり、body_entered 系のシグナルで検知する
## false: 手動で start_fall_sequence() を呼び出して使う(スイッチ床など)

@export
var target_groups: Array[String] = ["player"]
## このグループに属するノードが乗ったときだけ発動させたい場合に使用。
## 空配列の場合、グループ判定を行わない。

@export_group("Debug")
@export
var debug_print: bool = false

# 内部状態
var _original_parent_mode: PhysicsBody2D.Mode = PhysicsBody2D.MODE_STATIC
var _original_position: Vector2
var _elapsed_since_trigger: float = 0.0
var _is_triggered: bool = false
var _is_shaking: bool = false
var _has_fallen: bool = false
var _shake_offset: float = 0.0
var _parent_body: PhysicsBody2D

func _ready() -> void:
	# 親ノードを取得してチェック
	_parent_body = _find_parent_body()
	if _parent_body == null:
		push_warning("FallingPlatform: 親に PhysicsBody2D が見つかりません。このコンポーネントは PhysicsBody2D 系ノードにアタッチしてください。")
		return

	_original_parent_mode = _parent_body.mode
	_original_position = _parent_body.global_position

	# 自動検知モードの場合、シグナル接続を試みる
	if use_body_enter_signal:
		_connect_body_entered_signal()

	if debug_print:
		print("FallingPlatform: ready. parent=", _parent_body.name, " mode=", _original_parent_mode)


func _physics_process(delta: float) -> void:
	if not _is_triggered or _parent_body == null:
		return

	_elapsed_since_trigger += delta

	# 揺れ開始判定
	if not _is_shaking and _elapsed_since_trigger >= shake_delay:
		_is_shaking = true
		_shake_offset = 0.0
		if debug_print:
			print("FallingPlatform: shaking start")

	# 落下開始判定
	if not _has_fallen and _elapsed_since_trigger >= fall_delay:
		_start_fall()
		return

	# 揺れ処理
	if _is_shaking and not _has_fallen and shake_amplitude > 0.0 and shake_frequency > 0.0:
		_apply_shake(delta)


func _apply_shake(delta: float) -> void:
	# シンプルな左右揺れ(sin 波)
	_shake_offset += delta * TAU * shake_frequency
	var offset_x := sin(_shake_offset) * shake_amplitude
	# global_position 基準で揺らす
	_parent_body.global_position = _original_position + Vector2(offset_x, 0.0)


func _start_fall() -> void:
	if _has_fallen:
		return
	_has_fallen = true
	_is_shaking = false

	if debug_print:
		print("FallingPlatform: fall start")

	# 親を物理挙動化
	_parent_body.mode = PhysicsBody2D.MODE_RIGID

	# もし RigidBody2D ではない場合、Godot が内部的に扱えるように mode 変更だけで簡易的な落下が起こる
	# より厳密にやりたい場合は、RigidBody2D に差し替える処理を自前で書いてもよい

	# 自動削除設定
	if auto_free_after_fall > 0.0:
		call_deferred("_queue_free_later")


func _queue_free_later() -> void:
	# Timer を使わずに簡易的に遅延削除
	await get_tree().create_timer(auto_free_after_fall).timeout
	if is_instance_valid(_parent_body) and one_time_use:
		if debug_print:
			print("FallingPlatform: queue_free parent")
		_parent_body.queue_free()
	queue_free() # 自分自身も削除


func _find_parent_body() -> PhysicsBody2D:
	var p := get_parent()
	while p != null:
		if p is PhysicsBody2D:
			return p
		p = p.get_parent()
	return null


func _connect_body_entered_signal() -> void:
	# 親が Area2D の場合: body_entered シグナル
	if _parent_body is Area2D:
		var area := _parent_body as Area2D
		if not area.body_entered.is_connected(_on_body_entered):
			area.body_entered.connect(_on_body_entered)
		return

	# 親が PhysicsBody2D (StaticBody2D, CharacterBody2D, RigidBody2D) の場合:
	# 直接 body_entered シグナルはないので、
	# - 親に Area2D を子として付けて、そこからシグナルを飛ばす構成を推奨
	# ここでは、子孫の Area2D を探して自動接続を試みる
	var area2d := _parent_body.get_node_or_null("Detector")
	if area2d == null:
		# 名前 "Detector" に限定せず、最初に見つけた Area2D に接続する
		area2d = _parent_body.find_child("Area2D", true, false)
	if area2d and area2d is Area2D:
		var a := area2d as Area2D
		if not a.body_entered.is_connected(_on_body_entered):
			a.body_entered.connect(_on_body_entered)
	else:
		push_warning("FallingPlatform: body_entered を受け取る Area2D が見つかりません。use_body_enter_signal=false にして手動トリガにするか、Area2D を追加してください。")


func _on_body_entered(body: Node) -> void:
	if debug_print:
		print("FallingPlatform: body_entered from ", body.name)

	if _is_triggered:
		return

	# グループフィルタ
	if target_groups.size() > 0:
		var matched := false
		for g in target_groups:
			if body.is_in_group(g):
				matched = true
				break
		if not matched:
			return

	start_fall_sequence()


## 外部から明示的に呼び出して落下シーケンスを開始する
## 例: スイッチを押したら落ちる床など
func start_fall_sequence() -> void:
	if _is_triggered or _parent_body == null:
		return
	_is_triggered = true
	_elapsed_since_trigger = 0.0
	_original_position = _parent_body.global_position
	if debug_print:
		print("FallingPlatform: triggered")

使い方の手順

ここでは 2D プラットフォーマーを想定して、「プレイヤーが乗ると揺れて落ちる床」を例にします。

シーン構成例

典型的な床シーンはこんな感じにしておくと扱いやすいです。

FallingFloor (StaticBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Detector (Area2D)
 │    └── CollisionShape2D
 └── FallingPlatform (Node)

ポイントは:

  • 床の本体は StaticBody2D(最初は動かない床)
  • プレイヤーが乗ったことを検知するための Detector(Area2D)
  • 挙動ロジックは FallingPlatform コンポーネント に全て押し込む

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

  1. 上記の GDScript を res://components/falling_platform.gd などに保存します。
  2. Godot エディタで開くと、class_name FallingPlatform により、ノード追加ダイアログから直接追加可能になります。

手順②: 床シーンを作る

  1. 新規シーン → ルートに StaticBody2D を追加し、名前を FallingFloor にする。
  2. 子ノードとして Sprite2DCollisionShape2D を追加し、見た目と当たり判定を設定。
  3. さらに子ノードとして Area2D を追加し、名前を Detector に変更。
  4. Detector の下に CollisionShape2D を追加し、床と同じくらいの大きさにする(少し上に広げると「乗った瞬間」を拾いやすい)。

手順③: FallingPlatform コンポーネントをアタッチ

  1. ルートの FallingFloor (StaticBody2D) を選択。
  2. 右クリック → 「子ノードを追加」 → 検索欄に「FallingPlatform」と入力。
  3. FallingPlatform ノードを追加。
  4. インスペクタで以下のようにパラメータを設定:
    • shake_delay = 0.5
    • fall_delay = 1.0
    • shake_amplitude = 4.0(好みに応じて)
    • shake_frequency = 15.0
    • use_body_enter_signal = true
    • target_groups = ["player"](プレイヤーが player グループに属している前提)

これで、プレイヤーが Detector の当たり判定に入ると:

  • 0.5 秒後に床が左右に揺れ始める
  • 1.0 秒後に床が物理挙動化して落下する

手順④: プレイヤー側は一切いじらない

このコンポーネントの良いところは、プレイヤーのスクリプトを一切変更しなくていいところです。
プレイヤーはいつも通り CharacterBody2D で動いているだけで、床が勝手に落ちてくれます。

プレイヤーシーンの例:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── Camera2D

Player ノードに player グループを付けておけば、FallingPlatform が自動的に検知してくれます。


別パターン:敵専用の落ちる足場にする

同じコンポーネントをそのまま使って、敵専用の足場も簡単に作れます。

EnemyOnlyFloor (StaticBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Detector (Area2D)
 │    └── CollisionShape2D
 └── FallingPlatform (Node)

ここで FallingPlatformtarget_groups["enemy"] にすれば:

  • プレイヤーが乗っても落ちない
  • 敵(enemy グループに属するノード)が乗ると落ちる

という、ちょっとイヤらしいギミックもすぐ作れますね。


メリットと応用

この FallingPlatform コンポーネントを使うと、こんなメリットがあります。

  • 継承地獄からの開放
    「落ちる床シーン」「揺れる床シーン」「敵専用床シーン」…とシーンを増やすのではなく、
    床は床シーンのままで、FallingPlatform を付けるかどうか・パラメータをどうするかで挙動を変えられます。
  • シーン構造がシンプル
    ルートはあくまで StaticBody2D 1 個で、余計なスクリプトはコンポーネントに集約。
    レベルデザイン時に「これはどの床だっけ?」と迷いにくくなります。
  • 再利用性が高い
    既存の床シーンにも、あとから FallingPlatform ノードをポンと追加するだけで落下床化できます。
  • プレイヤーや敵のコードが肥大化しない
    「床の挙動」は床側で完結しているので、キャラクター側はシンプルに保てます。

応用案としては:

  • 落下前に AnimationPlayer で専用の揺れアニメを再生する
  • 落下後、一定時間経ったら元の位置に戻して「復活する床」にする
  • スイッチやトラップから start_fall_sequence() を呼び出して、「遠隔で落とす床」にする

改造案:時間経過で元の位置に戻る「復活床」にする

例えば、落下してから 3 秒後に元の位置に戻るような改造は、こんな関数を追加するだけで実現できます。


@export_range(0.0, 20.0, 0.1)
var respawn_delay: float = 0.0
## 0 より大きい値を設定すると、「落下後に元の位置へ戻る」復活床モードになる。

func _start_fall() -> void:
	if _has_fallen:
		return
	_has_fallen = true
	_is_shaking = false

	if debug_print:
		print("FallingPlatform: fall start (with respawn check)")

	_parent_body.mode = PhysicsBody2D.MODE_RIGID

	if respawn_delay > 0.0:
		_respawn_later()
	elif auto_free_after_fall > 0.0:
		call_deferred("_queue_free_later")


func _respawn_later() -> void:
	await get_tree().create_timer(respawn_delay).timeout
	if not is_instance_valid(_parent_body):
		return

	# 位置とモードをリセット
	_parent_body.global_position = _original_position
	_parent_body.linear_velocity = Vector2.ZERO
	_parent_body.angular_velocity = 0.0
	_parent_body.rotation = 0.0
	_parent_body.mode = _original_parent_mode

	# 状態を初期化して再利用
	_is_triggered = false
	_is_shaking = false
	_has_fallen = false
	_elapsed_since_trigger = 0.0

	if debug_print:
		print("FallingPlatform: respawned and ready again")

このように、挙動をすべてコンポーネントに閉じ込めておけば、「落ちる床」の仕様変更やバリエーション追加も、1 ファイルをいじるだけで済みます。
継承ベースで床シーンを量産するよりも、ずっとメンテしやすい構成になりますね。