Godot 4 でゲームを作っていると、こんなことありませんか?
- 「敵がダメージを受けたら、近くの敵に『警戒モードに入れ!』って知らせたい」
- 「スイッチが押されたら、同じグループのドアを全部開けたい」
- 「ボスがフェーズ移行したら、雑魚敵全員に『自爆せよ』イベントを飛ばしたい」
Godot 標準のやり方だと、だいたい次の2択になりますよね。
- シーンツリーをたどって直接参照する
get_tree().get_root().get_node("World/Enemies")みたいなコードで、特定パスのノードを取ってきて for 文で回す。 - signal を1対多でつなぐ
ボスの signal を全ての敵に接続して、敵側でハンドラを書く。
どちらも動きますが、だんだんこうなります。
- シーン構造をちょっと変えただけで
get_node()のパスが壊れる - signal の接続先が増えすぎて、「誰が誰に通知しているのか」追えなくなる
- 敵やスイッチのスクリプトが「通知ロジック」で太っていく
そこで、「継承より合成」の考え方で、グループ単位の通知だけを担当する小さなコンポーネントを作ってしまいましょう。
今回紹介する 「GroupSignaler」コンポーネント は、特定のタイミングで、指定グループの全ノードにメソッドコールを一斉送信するための汎用パーツです。
【Godot 4】グループ全員に一斉コール!「GroupSignaler」コンポーネント
このコンポーネントをアタッチしておけば、例えば:
- Enemy グループに
"enter_alert_state"メソッドを一斉送信 - Door グループに
"open_door"メソッドを一斉送信 - UI グループに
"show_message"メソッドを一斉送信
といった処理を、シーン構造に依存せず、signal の配線地獄にもハマらずに実現できます。
GDScript フルコード(GroupSignaler.gd)
extends Node
class_name GroupSignaler
## グループに対して一括でメソッド呼び出しを行うコンポーネント。
## 「誰にどう通知するか」をこのコンポーネントに閉じ込めて、
## 本体スクリプトをシンプルに保つのが狙いです。
## 通知先となるグループ名。
## 例: "Enemy", "Door", "UI" など。
@export var target_group: StringName = &"Enemy"
## 呼び出すメソッド名。
## グループ内のノードがこのメソッドを持っていれば呼び出されます。
## 例: "enter_alert_state", "open_door" など。
@export var method_name: StringName = &"on_group_signal"
## メソッドに渡す引数。
## 任意の Variant の配列を指定できます。
## 例: ["alert", 1.5] や [self] など。
@export var method_args: Array = []
## このコンポーネントが ready になったタイミングで自動的に通知を送るかどうか。
## レベル開始時に一括初期化したい場合などに便利です。
@export var emit_on_ready: bool = false
## このコンポーネントが削除される直前に通知を送るかどうか。
## 「死ぬときに仲間に知らせる敵」などに使えます。
@export var emit_on_tree_exiting: bool = false
## 通知時に、対象ノードがメソッドを持っていなくてもエラーにしない。
## true の場合、存在しないメソッドは無視されます(安全モード)。
@export var ignore_missing_method: bool = true
## 通知の対象から自分自身を除外するかどうか。
## 例: Enemy グループ内の1体が「他の敵だけ」に知らせたい場合に true。
@export var exclude_self: bool = false
## 通知を遅延実行するためのタイマー時間(秒)。
## 0 の場合は即時実行。それ以外の場合は await で待ってから通知します。
@export_range(0.0, 10.0, 0.1)
@export var delay_seconds: float = 0.0
## デバッグ用フラグ。true のとき、通知内容を出力します。
@export var debug_print: bool = false
func _ready() -> void:
if emit_on_ready:
emit_group_signal()
func _exit_tree() -> void:
if emit_on_tree_exiting:
# exit_tree 内で await は使えないので、遅延は無視して即時実行します。
_emit_group_signal_internal(false)
## 外部から明示的に呼び出すためのメイン API。
## 例: ボスの script から
## $GroupSignaler.emit_group_signal()
## のようにコールします。
func emit_group_signal() -> void:
if delay_seconds > 0.0:
# 遅延実行(コルーチン)
_emit_group_signal_with_delay()
else:
_emit_group_signal_internal(true)
## 遅延つきで通知を送る内部処理。
## 同期的に待ちたくないので、戻り値は特に返しません。
func _emit_group_signal_with_delay() -> void:
# Timer を使わず、シンプルに SceneTreeTimer を利用します。
var timer := get_tree().create_timer(delay_seconds)
await timer.timeout
_emit_group_signal_internal(true)
## 実際にグループに対してメソッドを呼び出す中核処理。
## @param allow_delay_log 遅延実行時にもログを出すかどうか。
func _emit_group_signal_internal(allow_delay_log: bool) -> void:
if target_group.is_empty():
push_warning("GroupSignaler: target_group is empty. No signal emitted.")
return
if method_name.is_empty():
push_warning("GroupSignaler: method_name is empty. No signal emitted.")
return
# 対象グループ内のノード一覧を取得
var nodes: Array = get_tree().get_nodes_in_group(target_group)
if debug_print:
var where := "immediate"
if delay_seconds > 0.0 and allow_delay_log:
where = "delayed (%.2f sec)" % delay_seconds
print("GroupSignaler: emitting to group '%s' (%d nodes), method='%s', mode=%s"
% [target_group, nodes.size(), method_name, where])
for node in nodes:
if exclude_self and node == self.get_owner_or_null():
# owner が通知元本体になるケースを想定して除外
continue
# メソッドが存在するかチェック
if ignore_missing_method and not node.has_method(method_name):
continue
# メソッド呼び出し
# callv は配列引数をそのまま可変長引数として渡せます。
var result := node.callv(method_name, method_args)
if debug_print:
# 戻り値も一応ログに出しておくとデバッグに便利
print(" - called on %s, result=%s" % [str(node), str(result)])
## ユーティリティ: owner を返すが、存在しなければ null。
## コンポーネントとして利用される前提なので、owner が本体ノードになります。
func get_owner_or_null() -> Node:
return owner if owner != null else self
使い方の手順
ここでは具体例として、「敵が倒されたとき、Enemy グループ全員を警戒モードにする」ケースで説明します。
例1: 敵が死んだら、他の敵に「警戒開始」を通知
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── GroupSignaler (Node)
- Enemy シーンにコンポーネントを追加
Enemy シーンを開き、Nodeを1つ追加してGroupSignaler.gdをアタッチします。
さらに、Enemy のルートノード(CharacterBody2D など)を 「Enemy」グループに登録しておきます。
(インスペクタの「ノード」タブ → グループ → 追加) - GroupSignaler の設定
GroupSignaler ノードを選択し、インスペクタで次のように設定します。target_group:Enemymethod_name:enter_alert_statemethod_args: 空のままで OK(引数なし)emit_on_ready:falseemit_on_tree_exiting:true(死ぬときに通知したい場合)exclude_self:true(自分自身には通知しない)debug_print: 必要に応じてtrue
- Enemy スクリプト側に受け取りメソッドを実装
Enemy のルートノードにアタッチしているスクリプト(例:Enemy.gd)に、enter_alert_state()を定義します。
# Enemy.gd (例)
extends CharacterBody2D
var is_alert: bool = false
func enter_alert_state() -> void:
if is_alert:
return
is_alert = true
print("%s: enter_alert_state!" % name)
# ここでアニメーションや移動速度変更などを行う
# $AnimationPlayer.play("alert")
- 死亡時に GroupSignaler が動くようにする
Enemy が HP0 になったときにqueue_free()するような実装にしておけば、emit_on_tree_exiting = trueの GroupSignaler が、自動的にグループへ通知を送ってくれます。
# Enemy.gd の一部例
var hp: int = 3
func take_damage(amount: int) -> void:
hp -= amount
if hp <= 0:
die()
func die() -> void:
print("%s: died" % name)
# ここで GroupSignaler の emit_on_tree_exiting が発火し、
# Enemy グループ全員に enter_alert_state() が送られる
queue_free()
これで、「誰に通知するか」「どうやって通知するか」といったロジックは GroupSignaler に閉じ込められ、Enemy.gd は 「自分の状態管理」だけに集中できます。
例2: スイッチが押されたら Door グループのドアを全開にする
Switch (Area2D) ├── Sprite2D ├── CollisionShape2D └── GroupSignaler (Node) Door (StaticBody2D) ├── Sprite2D └── CollisionShape2D
- Door シーンのルートノードを 「Door」グループに登録する。
- Door.gd に
open_door()メソッドを実装する。 - Switch シーンに GroupSignaler を追加し、次のように設定:
target_group:Doormethod_name:open_dooremit_on_ready:falseemit_on_tree_exiting:false
- スイッチが押されたタイミングで
emit_group_signal()を呼ぶ。
# Switch.gd
extends Area2D
@onready var group_signaler: GroupSignaler = $GroupSignaler
func _on_body_entered(body: Node) -> void:
if body.is_in_group("Player"):
print("Switch pressed by %s" % body.name)
group_signaler.emit_group_signal()
メリットと応用
この GroupSignaler コンポーネントを使うと、次のようなメリットがあります。
- シーン構造に依存しない
get_node("World/Enemies")のようなパス書きは不要。
グループさえ合っていれば、どこに配置されていても通知が届きます。 - signal の配線がシンプルになる
「ボス → 敵1, 敵2, 敵3, …」といった個別接続をやめて、
「ボス → GroupSignaler → Enemy グループ」という1本の線に集約できます。 - 使い回しやすい
「グループにメソッドを一斉送信する」という汎用的な機能だけを持つので、
敵、スイッチ、トリガーゾーン、UI 管理など、どんなシーンにもポン付けできます。 - コンポーネント指向でスクリプトが痩せる
「自分のロジック」と「通知のロジック」を分離できるので、
各スクリプトがシンプルになり、テストや差し替えも楽になります。
特に、「レベルデザイン時にシーン構造をガンガンいじるタイプのプロジェクト」では、
ノードパスに依存しないこのスタイルはかなり効いてきます。
改造案:条件付きで通知先をフィルタする
もう少し高度なことをしたい場合、例えば「Enemy グループのうち、画面内にいる敵だけに通知したい」などがあります。
そんなときは、GroupSignaler に filter_func 的なコールバックを追加するのも手です。
イメージとしては、次のようなメソッドを追加します:
## カスタムフィルタを使って通知先を絞り込みたい場合にオーバーライドする。
## デフォルト実装では常に true(全員対象)。
func _filter_target(node: Node) -> bool:
return true
そして _emit_group_signal_internal() 内で:
for node in nodes:
if exclude_self and node == self.get_owner_or_null():
continue
if not _filter_target(node):
continue
if ignore_missing_method and not node.has_method(method_name):
continue
node.callv(method_name, method_args)
このようにしておけば、必要になったときだけ GroupSignaler を継承してフィルタロジックを差し替えることができます。
「継承より合成」が基本ですが、「小さくて完結したコンポーネント」をさらに薄く継承して拡張するのは、十分アリなパターンですね。
ぜひ、自分のプロジェクト用にカスタマイズした GroupSignaler を1つ作っておいて、
「グループ通知は全部これ経由」というルールにしてしまいましょう。シーン構造もスクリプトも、かなりスッキリするはずです。
