【Godot】グループによる柔軟なオブジェクト判定

作成: 2025-06-20最終更新: 2025-12-16

Godotのグループ機能は、ノードにタグを付けて柔軟に管理する仕組みです。基本的な使い方から、パフォーマンスを意識した実践的な活用法、代替手法との比較までを解説します。

概要

Godotでゲーム開発を進める中で、「すべての敵に同じ命令を送りたい」「プレイヤーの攻撃が当たったのが『敵』なのか『アイテム』なのか判定したい」といった場面に遭遇します。ノードの親子関係やクラス名だけでオブジェクトを管理しようとすると、コードが複雑化し、保守性を低下させる原因となります。

これらの課題をエレガントに解決するのが、Godotのグループ (Groups) 機能です。

グループとは?柔軟な「タグ付け」システム

グループ設定画面

グループは、シーンツリーの階層やノードの種類とは独立して、ノードに仮想的な 「タグ」 を付けるためのシステムです。例えば、CharacterBody2Dのノードに「enemies」や「players」というタグを付けたり、Area2Dに「collectible_items」というタグを付けたりできます。

グループの主なメリット:

  • 柔軟な分類: 親子関係に縛られず、「役割」や「属性」といった意味的なまとまりでノードを分類できます。
  • 一括操作: 「enemies」グループに属する全てのノードを取得し、同じメソッドを呼び出すといった処理が簡単に記述できます。
  • 疎結合な設計: ノード同士が直接互いを参照しなくても、グループという共通の関心事を通じて連携できます。

グループの設定方法:エディタとコード

グループへの追加は、主に2つの方法で行います。

1. エディタで設定する(静的なノード向け)

シーンに配置済みのノードにグループを設定する最も手軽な方法です。

  1. シーンツリーで対象のノードを選択します。
  2. インスペクタの隣にある「ノード」タブを開きます。
  3. 「グループ」サブタブを選択します。
  4. テキストボックスにグループ名(例: enemies)を入力し、「追加」ボタンを押します。

2. コードで設定する(動的なノード向け)

ゲーム中に動的に生成されるノード(弾、エフェクトなど)をグループに追加する場合に用います。

# Bullet.gd
func _ready():
    # このノード(弾)を "player_bullets" グループに追加する
    add_to_group("player_bullets")

# ターゲットにヒットしたらグループから抜ける(任意)
func _on_hit_target():
    # 処理が不要になったらグループから削除
    remove_from_group("player_bullets")
    queue_free()

実践的な活用シナリオとコード例

シナリオ1:柔軟な衝突判定システム

is_in_group()は、衝突した相手が何者かを判定する際の常套手段です。if body.name == "..."のようなハードコーディングを避け、拡張性の高いコードを書きましょう。

# PlayerAttackArea.gd (Area2D)
# プレイヤーの剣の当たり判定エリア

func _on_body_entered(body: Node):
    # 衝突相手が "enemies" グループに属しているか?
    if body.is_in_group("enemies") and body.has_method("take_damage"):
        body.take_damage(attack_power)
        print("敵にヒット!")

    # 衝突相手が "destructible_objects" グループに属しているか?
    elif body.is_in_group("destructible_objects") and body.has_method("destroy"):
        body.destroy()
        print("オブジェクトを破壊!")

このコードの美点は、PlayerAttackAreaが攻撃対象の具体的なクラス(SlimeGoblinなど)を一切知る必要がない点です。新しい敵を追加する際も、このスクリプトを修正する必要はなく、新しいノードを適切なグループに追加するだけで済みます。

シナリオ2:グループへの一括命令(ブロードキャスト)

get_tree().call_group()を使えば、特定のグループに属する全ノードのメソッドを一斉に呼び出せます。

注意: call_group()で指定したメソッドが対象ノードに存在しない場合、エラーにはなりませんが何も実行されません。グループ内のノードが確実に該当メソッドを持つようにするか、has_method()でチェックするパターンを検討してください。

# AlarmSystem.gd
# 警報システムが作動した
func _on_alarm_triggered():
    print("警報作動!全警備員に通知します。")
    # "guards" グループに属する全ノードの "enter_alert_mode" メソッドを呼び出す
    get_tree().call_group("guards", "enter_alert_mode", get_player_last_known_position())

# Guard.gd (CharacterBody2D)
func enter_alert_mode(target_position: Vector2):
    state = STATE.ALERT
    nav_agent.target_position = target_position
    print("%sが警戒モードに移行。目標地点: %s" % [name, target_position])

func _ready():
    add_to_group("guards")

この例では、AlarmSystemはシーン内に何人の警備員がいるか、どこにいるかを全く気にする必要がありません。


よくある間違いとベストプラクティス

よくある間違いベストプラクティス
_process内でget_nodes_in_group()を呼び出す_ready()でリストをキャッシュするか、シグナルやcall_groupを使う。get_nodes_in_group()はシーン全体を探索するため、毎フレームの呼び出しは高コストです。
あらゆる判定にグループを使いすぎる役割に応じてメタデータやクラス名と使い分ける。グループは動的な「役割」に適しています。
グループ名に文字列を直接書き込む定数やシングルトン(自動読み込み)で管理する。const ENEMY_GROUP = "enemies"のように定数化すると、タイプミスや変更時の修正漏れを防げます。

パフォーマンスと代替手法との比較

グループは万能ではありません。状況によっては、他の機能を使った方がシンプルで高速な場合があります。

  • メタデータ (set_meta, get_meta): ノードに静的なキーと値のペア(例: "item_id": 101)を関連付けたい場合に適しています。グループのように一括検索する機能はありません。

  • クラス名 (class_name): if body is Enemy:のように型で直接判定できます。特定のクラスのインスタンスを扱いたい場合は明確ですが、「複数のクラスにまたがる役割」を表現するにはグループの方が柔軟です。

  • ダックタイピング (has_method): 相手の具体的な型やグループを知らずに、特定の振る舞い(メソッド)の有無で処理を分岐させたい場合に有効です。グループと組み合わせることで、より安全で柔軟なコードになります。

これらの手法は競合するものではなく、むしろ相補的な関係にあります。

まとめ

グループ機能は、オブジェクト管理を改善し、拡張性の高いコードベースを構築するための有用なツールです。

  • グループは、シーン階層から独立した柔軟なタグ付けシステムである。
  • is_in_group()衝突判定をスマートに行える。
  • call_group()で特定の役割を持つノード群に一斉に命令できる。
  • パフォーマンスを意識し、get_nodes_in_group()の乱用を避けることが重要。
  • メタデータやクラス名など、他の機能と適材適所で使い分けることで、より洗練された設計が可能になる。