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 グループに入れておきます。
- エディタで Player シーンを開く
- ルートノード(例:
Player (CharacterBody2D))を選択 - 右側の「Node > Groups」タブを開く
playerと入力して「Add」
シーン構成例:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── Camera2D
② マップにトリガー用 Area2D を置く
次に、ステージシーン側にトリガーエリアを追加します。
- マップシーン(例:
Stage1 (Node2D))を開く - 子ノードとして
Area2Dを追加し、名前をCutsceneTriggerAreaなどに変更 CollisionShape2Dを子に追加し、形状(RectangleShape2D など)とサイズを調整
シーン構成図:
Stage1 (Node2D)
├── TileMap
├── Player (CharacterBody2D)
└── CutsceneTriggerArea (Area2D)
└── CollisionShape2D
③ AreaTrigger コンポーネントをアタッチ
ここからが「合成」パートです。CutsceneTriggerArea に AreaTrigger コンポーネントをぶら下げます。
CutsceneTriggerArea (Area2D)を選択- 右クリック > 「子ノードを追加」 >
Nodeを追加 - その
Nodeに、先ほど作成したAreaTrigger.gdをスクリプトとしてアタッチ - インスペクタで
player_groupがplayerになっていることを確認(必要なら変更)
更新されたシーン構成図:
Stage1 (Node2D)
├── TileMap
├── Player (CharacterBody2D)
└── CutsceneTriggerArea (Area2D)
├── CollisionShape2D
└── AreaTrigger (Node) # <-- このスクリプト
④ シグナルをマップ側に接続してイベントを書く
最後に、AreaTrigger の triggered シグナルを、ステージシーンのスクリプトに接続します。
Stage1のルートノード(Stage1 (Node2D))にスクリプトをアタッチCutsceneTriggerArea/AreaTriggerを選択し、「Node」タブを開く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 以下にすれば「一度きり」、正の値にすれば「クールダウン付きで何度も起動」という挙動に切り替えられます。
こうやって「小さいコンポーネントを改造してバリエーションを作る」スタイルに慣れていくと、継承に頼らず柔軟なギミック設計ができるようになりますね。
