Godot 4 でゲームを作っていると、こんなことありませんか?

  • 「敵がダメージを受けたら、近くの敵に『警戒モードに入れ!』って知らせたい」
  • 「スイッチが押されたら、同じグループのドアを全部開けたい」
  • 「ボスがフェーズ移行したら、雑魚敵全員に『自爆せよ』イベントを飛ばしたい」

Godot 標準のやり方だと、だいたい次の2択になりますよね。

  1. シーンツリーをたどって直接参照する
    get_tree().get_root().get_node("World/Enemies") みたいなコードで、特定パスのノードを取ってきて for 文で回す。
  2. 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)
  1. Enemy シーンにコンポーネントを追加
    Enemy シーンを開き、Node を1つ追加して GroupSignaler.gd をアタッチします。
    さらに、Enemy のルートノード(CharacterBody2D など)を 「Enemy」グループに登録しておきます。
    (インスペクタの「ノード」タブ → グループ → 追加)
  2. GroupSignaler の設定
    GroupSignaler ノードを選択し、インスペクタで次のように設定します。
    • target_group: Enemy
    • method_name: enter_alert_state
    • method_args: 空のままで OK(引数なし)
    • emit_on_ready: false
    • emit_on_tree_exiting: true(死ぬときに通知したい場合)
    • exclude_self: true(自分自身には通知しない)
    • debug_print: 必要に応じて true
  3. 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")
  1. 死亡時に 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
  1. Door シーンのルートノードを 「Door」グループに登録する。
  2. Door.gd に open_door() メソッドを実装する。
  3. Switch シーンに GroupSignaler を追加し、次のように設定:
    • target_group: Door
    • method_name: open_door
    • emit_on_ready: false
    • emit_on_tree_exiting: false
  4. スイッチが押されたタイミングで 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つ作っておいて、
「グループ通知は全部これ経由」というルールにしてしまいましょう。シーン構造もスクリプトも、かなりスッキリするはずです。