Godot 4でドアを開ける「スイッチ」を作ろうとすると、ついこうなりがちですよね。

  • プレイヤーシーンのスクリプトに「ドアを開ける処理」を直接書く
  • ドア側から「プレイヤーが乗っているか」を監視する
  • ノード階層を深くして Area2DCollisionShape2D を継承した専用シーンを量産する

これでも動きますが、

  • 「敵が乗っても反応してほしい」「箱でも反応してほしい」など条件が増えるとスクリプトが肥大化
  • シーンごとに微妙に違うロジックが増えて、再利用しづらい
  • ドア側とスイッチ側が強く結合してしまい、あとから差し替えにくい

といった「継承&密結合あるある」にハマりがちです。

そこで今回は、どんなノードにもポン付けできる「感圧スイッチ」コンポーネントとして、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 シーンを作る

  1. 新規シーンを作成し、ルートに Area2D を追加
  2. 子として CollisionShape2D を追加し、長方形などで踏み台の範囲を設定
  3. 必要なら Sprite2D で見た目も追加
  4. ルートの Area2D に、上記の PressurePlate.gd をアタッチ
  5. シーンを PressurePlate.tscn として保存
PressurePlate (Area2D)
 ├── CollisionShape2D
 └── Sprite2D (任意)

この時点で、Area2D 自体が「PressurePlate コンポーネントを持つ感圧スイッチ」になっています。

手順②:プレイヤー・箱・ドアシーンを用意する

例として、以下のような構成を想定します。

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

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

Door (Node2D)
 ├── Sprite2D
 └── CollisionShape2D
  • PlayerCrate をそれぞれ "weight" グループに入れておきます。
    • エディタでノードを選択 → 「Node」タブ → Groups → weight を追加
  • 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 … 乗っている間だけON
  • behavior_required_bodies = 1 … 1つ乗ればOK

最後に、シグナルをつなぎます。

  1. PressurePlate を選択 → Node タブ → pressure_changed(pressed: bool) シグナルをダブルクリック
  2. 接続先として 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 みたいにコンポーネントで切り出せないかな?」と一度考えてみると、プロジェクト全体がかなりスッキリしますよ。