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 コンポーネント に全て押し込む
手順①: コンポーネントスクリプトを用意する
- 上記の GDScript を
res://components/falling_platform.gdなどに保存します。 - Godot エディタで開くと、
class_name FallingPlatformにより、ノード追加ダイアログから直接追加可能になります。
手順②: 床シーンを作る
- 新規シーン → ルートに
StaticBody2Dを追加し、名前をFallingFloorにする。 - 子ノードとして
Sprite2DとCollisionShape2Dを追加し、見た目と当たり判定を設定。 - さらに子ノードとして
Area2Dを追加し、名前をDetectorに変更。 Detectorの下にCollisionShape2Dを追加し、床と同じくらいの大きさにする(少し上に広げると「乗った瞬間」を拾いやすい)。
手順③: FallingPlatform コンポーネントをアタッチ
- ルートの
FallingFloor (StaticBody2D)を選択。 - 右クリック → 「子ノードを追加」 → 検索欄に「FallingPlatform」と入力。
- FallingPlatform ノードを追加。
- インスペクタで以下のようにパラメータを設定:
shake_delay = 0.5fall_delay = 1.0shake_amplitude = 4.0(好みに応じて)shake_frequency = 15.0use_body_enter_signal = truetarget_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)
ここで FallingPlatform の target_groups を ["enemy"] にすれば:
- プレイヤーが乗っても落ちない
- 敵(
enemyグループに属するノード)が乗ると落ちる
という、ちょっとイヤらしいギミックもすぐ作れますね。
メリットと応用
この FallingPlatform コンポーネントを使うと、こんなメリットがあります。
- 継承地獄からの開放:
「落ちる床シーン」「揺れる床シーン」「敵専用床シーン」…とシーンを増やすのではなく、
床は床シーンのままで、FallingPlatform を付けるかどうか・パラメータをどうするかで挙動を変えられます。 - シーン構造がシンプル:
ルートはあくまでStaticBody2D1 個で、余計なスクリプトはコンポーネントに集約。
レベルデザイン時に「これはどの床だっけ?」と迷いにくくなります。 - 再利用性が高い:
既存の床シーンにも、あとから 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 ファイルをいじるだけで済みます。
継承ベースで床シーンを量産するよりも、ずっとメンテしやすい構成になりますね。
