Godot 4でドアを開ける「スイッチ」を作ろうとすると、ついこうなりがちですよね。
- プレイヤーシーンのスクリプトに「ドアを開ける処理」を直接書く
- ドア側から「プレイヤーが乗っているか」を監視する
- ノード階層を深くして
Area2DやCollisionShape2Dを継承した専用シーンを量産する
これでも動きますが、
- 「敵が乗っても反応してほしい」「箱でも反応してほしい」など条件が増えるとスクリプトが肥大化
- シーンごとに微妙に違うロジックが増えて、再利用しづらい
- ドア側とスイッチ側が強く結合してしまい、あとから差し替えにくい
といった「継承&密結合あるある」にハマりがちです。
そこで今回は、どんなノードにもポン付けできる「感圧スイッチ」コンポーネントとして、PressurePlate を用意してみましょう。
「上に物が乗っている間だけシグナルをONにする」挙動を、1つのコンポーネントに閉じ込めて、ドアなどはシグナルを受け取るだけにします。
【Godot 4】踏まれたらシグナルON!「PressurePlate」コンポーネント
以下が、コピペでそのまま使える PressurePlate.gd のフルコードです。
Area2D にアタッチして使う想定ですが、「コンポーネント」としての役割だけに集中させてあります。
extends Area2D
class_name PressurePlate
##
## PressurePlate (感圧スイッチ) コンポーネント
## - 上に「乗っている」ボディが1つ以上ある間だけ pressed = true
## - 状態変化時にシグナルを発火
## - ドアやトラップなどは、このシグナルを受け取るだけでOK
##
## --- シグナル定義 ---------------------------------------------------------
## プレートの状態が変化したときに発火
## pressed: true = 踏まれている, false = 誰も乗っていない
signal pressure_changed(pressed: bool)
## 押された瞬間だけ欲しい場合はこちら
signal pressed
## 離された瞬間だけ欲しい場合はこちら
signal released
## --- エディタから設定できるパラメータ -------------------------------
@export_group("Detection", "detect_")
## 反応させたいボディのグループ名
## 例: "player", "enemy", "crate" など
## 空文字にすると「グループ無視」で、何でも反応する
@export var detect_group: String = ""
## RigidBody2D などの「重さ」を見る場合に使う
## 最低重量(kg)未満のボディは無視する
## 0 の場合は「重さを見ない」
@export var detect_min_mass: float = 0.0
## 同時に乗れる最大数。0 の場合は無制限
## これを 1 にすると「1つだけ乗れるスイッチ」みたいな挙動にできる
@export var detect_max_bodies: int = 0
@export_group("Behavior", "behavior_")
## 踏まれている間だけ有効にするか?
## false にすると「一度踏まれたら押しっぱなし」のラッチ式スイッチとしても使える
@export var behavior_hold_to_press: bool = true
## 何個のボディが乗ったら「押された」とみなすか
## 例: 2 にすると「2つ以上の物体が乗ったらON」
@export_range(1, 99, 1) var behavior_required_bodies: int = 1
@export_group("Debug", "debug_")
## デバッグ表示用。true のとき、押されている間だけ色を変える
@export var debug_draw_state: bool = true
## 押されているときの色
@export var debug_pressed_color: Color = Color(0.3, 1.0, 0.3, 0.4)
## 離されているときの色
@export var debug_released_color: Color = Color(1.0, 0.3, 0.3, 0.4)
## --- 内部状態 -------------------------------------------------------------
## 現在プレートの上にいる「有効な」ボディの数
var _current_body_count: int = 0
## 現在の論理状態
var _is_pressed: bool = false
## ラッチ式のときに、一度押されたかどうか
var _latched: bool = false
func _ready() -> void:
## Body 入退場のシグナルを受け取る
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
## 初期状態を反映
_update_state()
## --- 公開プロパティ ------------------------------------------------------
## 現在踏まれているかどうかを外部からも読めるように
var pressed: bool:
get:
return _is_pressed
## --- 内部ロジック --------------------------------------------------------
func _on_body_entered(body: Node) -> void:
if not _is_body_valid(body):
return
if detect_max_bodies > 0 and _current_body_count >= detect_max_bodies:
## 上限を超える場合はカウントしない
return
_current_body_count += 1
_update_state()
func _on_body_exited(body: Node) -> void:
if not _is_body_valid(body):
return
_current_body_count = max(0, _current_body_count - 1)
_update_state()
func _is_body_valid(body: Node) -> bool:
## グループチェック
if detect_group != "" and not body.is_in_group(detect_group):
return false
## 質量チェック (RigidBody2D など)
if detect_min_mass > 0.0 and body is RigidBody2D:
var rb := body as RigidBody2D
if rb.mass < detect_min_mass:
return false
return true
func _update_state() -> void:
var new_pressed := false
if behavior_hold_to_press:
## 「乗っている間だけON」の普通の感圧スイッチ
new_pressed = _current_body_count >= behavior_required_bodies
else:
## ラッチ式: 一度条件を満たしたら pressed を維持
if _latched:
new_pressed = true
else:
new_pressed = _current_body_count >= behavior_required_bodies
if new_pressed:
_latched = true
if new_pressed == _is_pressed:
## 状態が変わっていなければ何もしない
return
_is_pressed = new_pressed
## シグナル発火
pressure_changed.emit(_is_pressed)
if _is_pressed:
pressed.emit()
else:
released.emit()
## デバッグ描画更新
if debug_draw_state:
queue_redraw()
func _draw() -> void:
if not debug_draw_state:
return
## CollisionShape2D の形が取れればそれを使うのがベストだが、
## ここでは簡易的に Area2D の原点中心に矩形を描画する
var color := debug_released_color
if _is_pressed:
color = debug_pressed_color
var rect := Rect2(Vector2(-16, -4), Vector2(32, 8))
draw_rect(rect, color, true)
## --- 公開API(任意で使える) -------------------------------------------
## 手動で「ラッチ状態」をリセットしたいときに呼ぶ
func reset_latch() -> void:
_latched = false
_update_state()
使い方の手順
ここでは典型的な例として、
- プレイヤーが乗るとドアが開く
- 箱(RigidBody2D)を乗せてもドアが開く
というシーンを作ってみます。
手順①:PressurePlate シーンを作る
- 新規シーンを作成し、ルートに
Area2Dを追加 - 子として
CollisionShape2Dを追加し、長方形などで踏み台の範囲を設定 - 必要なら
Sprite2Dで見た目も追加 - ルートの
Area2Dに、上記のPressurePlate.gdをアタッチ - シーンを
PressurePlate.tscnとして保存
PressurePlate (Area2D) ├── CollisionShape2D └── Sprite2D (任意)
この時点で、Area2D 自体が「PressurePlate コンポーネントを持つ感圧スイッチ」になっています。
手順②:プレイヤー・箱・ドアシーンを用意する
例として、以下のような構成を想定します。
Player (CharacterBody2D) ├── Sprite2D └── CollisionShape2D Crate (RigidBody2D) ├── Sprite2D └── CollisionShape2D Door (Node2D) ├── Sprite2D └── CollisionShape2D
- Player と Crate をそれぞれ
"weight"グループに入れておきます。- エディタでノードを選択 → 「Node」タブ → Groups →
weightを追加
- エディタでノードを選択 → 「Node」タブ → Groups →
- Door には簡単なスクリプトを付けて、「開く/閉じる」を制御します。
Door のシンプルな例:
extends Node2D
@export var open_position: Vector2 = Vector2(0, -64)
@export var closed_position: Vector2 = Vector2.ZERO
@export var move_speed: float = 4.0
var _target_position: Vector2
func _ready() -> void:
position = closed_position
_target_position = closed_position
func _process(delta: float) -> void:
position = position.lerp(_target_position, delta * move_speed)
func open() -> void:
_target_position = open_position
func close() -> void:
_target_position = closed_position
手順③:PressurePlate と Door を接続する
メインシーンで、以下のような構成にします。
Main (Node2D) ├── Player (CharacterBody2D) ├── Crate (RigidBody2D) ├── Door (Node2D) └── PressurePlate (Area2D) <-- さっき作ったシーンをインスタンス
次に、PressurePlate のインスタンスを選択し、インスペクタで以下のように設定します。
detect_group = "weight"… Player と Crate だけ反応させるdetect_min_mass = 0.0… 重さは気にしない(必要なら調整)behavior_hold_to_press = true… 乗っている間だけONbehavior_required_bodies = 1… 1つ乗ればOK
最後に、シグナルをつなぎます。
- PressurePlate を選択 → Node タブ →
pressure_changed(pressed: bool)シグナルをダブルクリック - 接続先として Door を選び、
_on_pressure_plate_pressure_changedなどの関数名で接続
Door スクリプト側に自動生成される関数を、次のように書きます。
func _on_pressure_plate_pressure_changed(pressed: bool) -> void:
if pressed:
open()
else:
close()
これで、Player か Crate がプレートの上にいる間だけドアが開くようになります。
手順④:敵専用スイッチ、重さ制限付きスイッチなどを量産する
同じ PressurePlate.tscn をインスタンスして、インスペクタのパラメータを変えるだけで、いろいろなバリエーションが作れます。
- 敵専用スイッチ:
detect_group = "enemy"- 敵ノードを
"enemy"グループに入れておく
- 重い箱だけ反応するスイッチ:
detect_group = "crate"detect_min_mass = 5.0などに設定
- 一度踏んだら押しっぱなしのラッチ式スイッチ:
behavior_hold_to_press = false- Door 側は「pressed = true のときだけ open() する」ようにしておく
例えば敵専用スイッチ用のシーン構成はこんな感じです。
EnemySwitch (Area2D) ├── CollisionShape2D └── PressurePlate (スクリプトとしてアタッチ済み)
中身は同じ PressurePlate.gd ですが、パラメータだけ変えて「敵専用」「箱専用」などを作れるのがコンポーネント指向の強みですね。
メリットと応用
PressurePlate コンポーネントを導入することで、以下のようなメリットがあります。
- ロジックの再利用性が高い
「乗っている間だけON」「誰が乗ったら反応するか」といったロジックが1か所にまとまります。
ドア、トラップ、動く床、隠し通路など、どこでも「シグナルを受け取るだけ」で済みます。 - シーン構造がスッキリする
プレイヤーや敵、箱などのシーンは「自分の動き」に集中でき、スイッチ判定は PressurePlate に丸投げできます。
深い継承ツリーや、「プレイヤーがドアを知っている」ような強い結合を避けられます。 - レベルデザインが楽になる
レベルデザイナーは、PressurePlate をポンと置いて、インスペクタでグループ名や必要人数を変えるだけ。
スクリプトを書き換えずに、ギミックのバリエーションを増やせます。
応用として、例えば「一定時間だけONを維持するタイマー付きプレート」に改造するのも簡単です。
以下は、プレートが離されたあとも cooldown_time 秒だけ ON を維持する改造案です。
@export_group("Behavior", "behavior_")
@export var behavior_hold_to_press: bool = true
@export var behavior_required_bodies: int = 1
@export var behavior_release_delay: float = 0.0 # <-- 追加: 離されてからOFFになるまでの遅延秒数
var _release_timer: float = 0.0
func _process(delta: float) -> void:
if not behavior_hold_to_press and behavior_release_delay <= 0.0:
return
if _current_body_count >= behavior_required_bodies:
_release_timer = behavior_release_delay
if not _is_pressed:
_set_pressed(true)
else:
if behavior_release_delay > 0.0 and _is_pressed:
_release_timer -= delta
if _release_timer <= 0.0:
_set_pressed(false)
func _set_pressed(value: bool) -> void:
if value == _is_pressed:
return
_is_pressed = value
pressure_changed.emit(_is_pressed)
if _is_pressed:
pressed.emit()
else:
released.emit()
if debug_draw_state:
queue_redraw()
このように、スイッチ系のギミックは「コンポーネント化」しておくと、あとから仕様変更やバリエーション追加がとても楽になります。
継承ツリーを増やす前に、「これ、PressurePlate みたいにコンポーネントで切り出せないかな?」と一度考えてみると、プロジェクト全体がかなりスッキリしますよ。
