Godot でイベントトリガーを作るとき、ついこんな実装をしがちですよね。

  • マップごとに専用の Area2D シーンを継承して作る
  • 「プレイヤーだけ反応する」処理を毎回コピペ
  • トリガー処理を書いたあと、消すかどうかを個別に実装

これを何十個もステージに置き始めると、「あれ、このマップのトリガーだけ挙動が違うぞ?」みたいな事故が起きがちです。
そこで今回は、「親の Area2D にアタッチするだけ」で完結する、コンポーネント指向なイベントトリガーコンポーネントを用意しました。

この AreaTrigger コンポーネント は:

  • 親の Area2D にプレイヤーが入ったら
  • シグナルを発火し
  • 一度きりのトリガーとして自動で自滅

という「イベント起動の基本セット」を、どのシーンからでも使い回せるようにしてくれます。
継承ツリーを増やさず、「合成(Composition)」でイベントを組み立てていきましょう。

【Godot 4】一度きりのイベント発火をコンポーネント化!「AreaTrigger」コンポーネント

フルコード(GDScript / Godot 4)


extends Node
class_name AreaTrigger
## 親の Area2D にプレイヤーが侵入したときにシグナルを出し、
## 一度だけ発火して自動的に自滅するコンポーネント。
##
## 想定ノード構成:
##   SomeTriggerArea (Area2D)
##    ├── CollisionShape2D
##    └── AreaTrigger (Node) <-- このスクリプト

## プレイヤー侵入時に発火するシグナル。
## 外側のシーン(マップなど)から接続して使います。
signal triggered(player: Node)

## どのノードを「プレイヤー」とみなすかを判定するためのグループ名。
## 例: Player シーンを "player" グループに入れておき、ここでも "player" と指定。
@export var player_group: StringName = &"player"

## すでに一度トリガーしたかどうかのフラグ。
## true になったら二度目以降は無視します。
var _already_triggered: bool = false

func _ready() -> void:
    # 親ノードが Area2D であることを前提にしているので、チェックしておきます。
    var area_parent := get_parent()
    if area_parent == null or not area_parent is Area2D:
        push_warning("AreaTrigger must be a child of an Area2D. Current parent: %s" % [area_parent])
        return

    # 親 Area2D の body_entered シグナルに接続します。
    # Godot 4 では Callable を使って接続するのが基本形です。
    var err := area_parent.body_entered.connect(_on_body_entered)
    if err != OK:
        push_warning("Failed to connect to parent Area2D.body_entered (error code: %s)" % [err])


func _on_body_entered(body: Node) -> void:
    # すでに一度トリガーしていたら何もせず return。
    if _already_triggered:
        return

    # プレイヤー判定:
    # - player_group が空なら「すべてのボディ」を対象
    # - 指定されていれば、そのグループに属しているかどうかをチェック
    if player_group != StringName() and not body.is_in_group(player_group):
        return

    # ここまで来たら「プレイヤーが侵入した」とみなす
    _already_triggered = true

    # シグナルを発火。外部からこのシグナルに接続して、イベントを組み立てます。
    emit_signal("triggered", body)

    # 自分自身(コンポーネント)を削除します。
    # 親の Area2D は残るので、必要なら別の用途に再利用も可能です。
    queue_free()


## --- おまけ:手動でリセットしたい場合用のユーティリティ ---

## 明示的にトリガー状態をリセットしたいときに呼び出します。
## デフォルトでは使いませんが、スイッチ型のギミックに流用する場合などに便利です。
func reset() -> void:
    _already_triggered = false

使い方の手順

ここでは「プレイヤーがエリアに入ったらカットシーンを再生して、そのトリガーは一度きり」という、よくあるケースを例に進めます。

① プレイヤーをグループに登録する

まず、プレイヤーシーンを player グループに入れておきます。

  1. エディタで Player シーンを開く
  2. ルートノード(例: Player (CharacterBody2D))を選択
  3. 右側の「Node > Groups」タブを開く
  4. player と入力して「Add」

シーン構成例:

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

② マップにトリガー用 Area2D を置く

次に、ステージシーン側にトリガーエリアを追加します。

  1. マップシーン(例: Stage1 (Node2D))を開く
  2. 子ノードとして Area2D を追加し、名前を CutsceneTriggerArea などに変更
  3. CollisionShape2D を子に追加し、形状(RectangleShape2D など)とサイズを調整

シーン構成図:

Stage1 (Node2D)
 ├── TileMap
 ├── Player (CharacterBody2D)
 └── CutsceneTriggerArea (Area2D)
      └── CollisionShape2D

③ AreaTrigger コンポーネントをアタッチ

ここからが「合成」パートです。CutsceneTriggerAreaAreaTrigger コンポーネントをぶら下げます。

  1. CutsceneTriggerArea (Area2D) を選択
  2. 右クリック > 「子ノードを追加」 > Node を追加
  3. その Node に、先ほど作成した AreaTrigger.gd をスクリプトとしてアタッチ
  4. インスペクタで player_groupplayer になっていることを確認(必要なら変更)

更新されたシーン構成図:

Stage1 (Node2D)
 ├── TileMap
 ├── Player (CharacterBody2D)
 └── CutsceneTriggerArea (Area2D)
      ├── CollisionShape2D
      └── AreaTrigger (Node)  # <-- このスクリプト

④ シグナルをマップ側に接続してイベントを書く

最後に、AreaTriggertriggered シグナルを、ステージシーンのスクリプトに接続します。

  1. Stage1 のルートノード(Stage1 (Node2D))にスクリプトをアタッチ
  2. CutsceneTriggerArea/AreaTrigger を選択し、「Node」タブを開く
  3. triggered シグナルをダブルクリックし、Stage1 に接続

自動生成された関数に、カットシーン再生などの処理を書きます。


extends Node2D

@onready var player: Node = $Player

func _on_area_trigger_triggered(player_body: Node) -> void:
    # ここに一度きりのイベント処理を書く
    # 例: カットシーン開始、UI 表示、セリフ再生など
    print("Cutscene start! Triggered by: ", player_body.name)

    # プレイヤー操作をロックする例
    if player_body.has_method("set_input_enabled"):
        player_body.set_input_enabled(false)

    # ここで別のシーンをインスタンスしてもOK
    # var cutscene := preload("res://cutscenes/intro_cutscene.tscn").instantiate()
    # add_child(cutscene)

これで、プレイヤーが CutsceneTriggerArea に入った瞬間に triggered シグナルが飛び、処理が終わったら AreaTrigger コンポーネント自身は自動で消えてくれます。

メリットと応用

  • 継承地獄からの解放
    「カットシーントリガー用 Area2D」「チュートリアルトリガー用 Area2D」…とシーンを分けて継承していく必要がありません。
    すべて Area2D + AreaTrigger という同じ構成で統一できます。
  • レベルデザインがシンプルになる
    マップシーンを開いたとき、「ここにトリガーがある」と一目で分かります。
    トリガーの中身(何をするか)は、triggered シグナル先のコードだけを見ればよいので、責務がきれいに分離されます。
  • 再利用性が高い
    プレイヤー検知&一度きりの起動というロジックは、どのマップでも共通です。
    それをコンポーネントに押し込めておくことで、「動かす側」はイベント内容だけに集中できます。
  • ノード階層が浅く保てる
    トリガーごとに専用シーンを作ると、インスタンス階層がどんどん深くなりがちです。
    今回のような「シンプルな Node コンポーネント」をぶら下げる方式なら、
    Area2D の直下にまとまってくれるので管理しやすいですね。

改造案:複数回トリガーできる「クールダウン付きスイッチ」にする

一度きりではなく、「一定時間ごとに何度も反応するトリガー」にしたい場合は、こんな関数を追加してみるのもアリです。


@export var cooldown_sec: float = 2.0
var _cooling_down: bool = false

func _on_body_entered(body: Node) -> void:
    if _already_triggered and cooldown_sec <= 0.0:
        return
    if _cooling_down:
        return
    if player_group != StringName() and not body.is_in_group(player_group):
        return

    _already_triggered = true
    emit_signal("triggered", body)

    if cooldown_sec > 0.0:
        _cooling_down = true
        await get_tree().create_timer(cooldown_sec).timeout
        _cooling_down = false
    else:
        queue_free()

cooldown_sec を 0 以下にすれば「一度きり」、正の値にすれば「クールダウン付きで何度も起動」という挙動に切り替えられます。
こうやって「小さいコンポーネントを改造してバリエーションを作る」スタイルに慣れていくと、継承に頼らず柔軟なギミック設計ができるようになりますね。