ノード間のやりとり(良い方法)

情報

本記事の元ネタとなったGodot Discordの@TheDurielによる原型図に心から感謝します。この資料は保存しておき、必要に応じて参照できる状態にしておくことをオススメします。

課題

プロジェクトが複雑化してきました。複数のシーン、インスタンス、そして膨大な数のノードが存在しています。以下のようなコードが沢山ありませんか。

get_node("../../SomeNode/SomeOtherNode")
get_parent().get_parent().get_node("SomeNode")
get_tree().get_root().get_node("SomeNode/SomeOtherNode")

このようにノード参照すると、すぐに問題が発生することに気づくでしょう。シーンツリーの何かを変更すると、これらの参照がすべて無効になる可能性があるからです。

もっと良い方法があります。ノード間やシーン間のやり取りをシンプルにしていきましょう。

解決策

基本的に、ノードはその子ノードを管理するべきです。逆の関係(子ノードが親ノードを管理する)は避ける必要があります。get_parent()get_node("..")を使用している場合、すでに問題が発生し始めています。このようなノードパスは 脆弱 であり、簡単に壊れてしまう性質があります。この構成には主に3つの重大な問題点があります。

  1. シーンを単独でテストすることはできません。そのシーン単体、あるいは厳密に同一のノード構成を持たないテストシーンで実行した場合、get_node() メソッドがクラッシュを引き起こします。

  2. 変更は簡単にはできません。ツリーの構造を変更する場合、パスが無効になる可能性があります。

  3. ノードの_ready()が呼ばれる順番は、子ノード優先、親ノード後回しとなります。つまり、ノードの _ready() メソッド内で親プロパティにアクセスしようとすると失敗するケースがあります。これは親ノードがまだ準備中(_ready()が呼ばれていない)状態であるためです。

ヒント

ノードがツリー構造にどのように追加され、準備完了状態になるかについては、ツリー順序を理解する を参照してください。

一般的に、ノードやシーンはゲーム内の任意の場所でインスタンス化可能であるべきであり、その親オブジェクトがどのようなものになるかについて一切仮定すべきではありません。

このチュートリアルでは後ほど詳細な例を紹介しますが、現時点でのノード間通信における「基本原則」は以下の通りです。

「下には呼び出しで」「上にはシグナルで」

ノードが子要素を呼び出している場合(つまりツリー構造を「下方向に」移動している場合)には、get_node() メソッドを使用するのが適切です。

ノードが「ツリー構造の上位」とやり取りする場合、シグナルを使用する方が適切でしょう。

シーン設定を設計する際にこのルールを念頭に置いておけば、メンテナンス性に優れ、整理されたプロジェクト構築への道筋が自然と見えてきます。また、問題を引き起こす煩雑なノードパスの使用も避けられるでしょう。

それでは、これらの戦略を具体例とともに見ていきます。

1. get_node() を使用する方法

get_node() は、指定されたパスを使用してシーンツリーを辿り、指定した名前のノードを検索します。

ヒント

ノードパスについてより詳しく知りたい場合は、ノードパスの理解を参照してください。

get_node() の使用例

以下の一般的な設定例について考えてみます。

alt alt

Player ノード内のスクリプトでは、プレイヤーの移動状況に応じて、AnimatedSprite2Dにどのアニメーションを再生すべきか通知する必要があります。このようなケースでは get_node() が適しています。

extends CharacterBody2D

func _process(delta):
    if speed > 0:
        get_node("AnimatedSprite2D").play("run")
    else:
        get_node("AnimatedSprite2D").play("idle")
ヒント

GDScriptでは、get_node()の省略形として$を使用できます。代わりに$AnimatedSprite2Dと記述してください。

より良い方法

このアプローチの欠点は、ノードパスを明示的に指定する必要があり、後でそのパスが変更された場合、コードも修正が必要になる点です。代わりに、@export 機能を使って直接ノードを選択することもできます。

extends CharacterBody2D

@export var animation : AnimatedSprite2D

func _process(delta):
    if speed > 0:
        animation.play("run")
    else:
        animation.play("idle")

この方法では、ノードを選択することで、インスペクター上で変数の値を直接割り当てることができます。

2. シグナルの活用方法

シグナルは、ツリーで上位に位置するノード、または同じ階層にあるノードに対して関数を呼び出すために使いましょう。

シグナルの接続はエディター内で行えます(通常はゲーム開始前に存在するノードに対して)、またはコード内で行うこともできます(実行時にインスタンス化するノードの場合)。シグナル接続の構文は以下の通りです。

signal_name.connect(target_node.target_function)

この問題を見ると、「上位に位置するノード、または同じ階層にあるノードに接続する場合、../Siblingのようなノードパスが必要になるのでは?」と思うかもしれません。その方法でも可能ですが、先ほどのルールに反します。この組み合わせの正解は、接続を共通の親ノードを介して行うことです。

ツリー構造を 下方向 に辿るルールに従うと、シグナル発信ノードと受信ノードの共通親ノードは、定義上それらの位置を認識しており、両ノードが準備完了した時点で待機状態になります。

シグナルの使用例

シグナルの最も一般的な使用例は、UI(ユーザーインターフェース)の更新です。プレイヤーのhealth変数が変化した際、対応するLabel(ラベル表示)やProgressBar(進捗バー)をリアルタイムに更新する必要があります。ただし、UIノードは通常プレイヤーオブジェクトから完全に分離されています(その方が設計上適切です)。プレイヤー側は、これらのノードがどこにあるのか、どのようにしてアクセスするのかを知る手段を一切持っていないはずです。

以下に例となる設定を示します。

alt alt

注:UIはインスタンス化されたシーンであり、実際には含まれるノードを表示しているに過ぎません。ここでよく見かけるのがget_node("../UI/VBoxContainer/HBoxContainer/Label).text = str(health)のようなコードで、これは避けるべき実装方法です。

代わりに、プレイヤーがHPを増減させるたびに、health_changedシグナルが発火します。これをUIのupdate_health()関数に送信する必要があり、この関数ではLabelの表示値を設定する処理を行います。Playerスクリプトでは、プレイヤーのHPが変更されるたびに以下のコードを使用しています。

health_changed.emit(health)

スクリプトUIには以下が含まれています。

@onready var label = $VBoxContainer/HBoxContainer/Label

func update_health(value):
    label.text = str(value)

これで signal を関数に接続するだけです。最適な接続場所は World クラスです。ここは共通の親ノードであり、両方のノードの位置を把握しているためです。

func _ready():
    $Player.health_changed.connect($UI.update_health)

3. グループを活用する方法

グループ化はモジュール分離のもう一つの有効な手段であり、特に類似した複数のオブジェクトで同じ処理が必要な場合に有効です。ノードはどのグループにも自由に追加でき、メンバーシップは「add_to_group()」および「remove_from_group()」で動的に変更できます。

グループに関するよくある誤解として、「ノード参照を『含む』ような何らかのオブジェクトや配列のようなもの」という認識があります。しかし実際にはグループはタグ管理システムです。特定のタグが割り当てられている場合、そのノードは「そのグループに属している」とみなされます。SceneTreeはこのタグ情報を管理しており、get_nodes_in_group()などの関数を使って、指定したタグを持つすべてのノードを効率的に検索できます。

グループの使い方

ギャラガ風のスペースシューティングゲームを考えてみます。このゲームでは、画面に多数の敵機がランダムに出現します。これらの敵には種類や挙動が異なる場合があります。「スマートボム」機能を追加し、これを発動すると画面内のすべての敵を一度に破壊できるようにしたいとします。この場合、グループ機能を活用することで、最小限のコードで実装ができます。

まず、すべての敵キャラを「enemies」グループに追加してください。これはエディターで「Node」タブを使用して行えます

alt alt

スクリプト内でグループにノードを追加することもできます。

func _ready():
    add_to_group("enemies")

敵キャラ全員が死亡時の処理(アニメーション再生、ドロップアイテムの生成など)するexplode()関数を持っていると仮定します。これで全ての敵がグループに登録されたので、スマートボム機能を以下のように実装できます。

func activate_smart_bomb():
    get_tree().call_group("enemies", "explode")

4. owner を使用した場合

owner はシーン保存時に自動的に設定される Node プロパティです。このシーン内のすべてのノードに対して、その owner にはシーンのルートノードが指定されます。これにより、子シグナルをメインノードにスムーズに接続できる便利な仕組みとなっています。

ownerの使用例

複雑なUI設計では、コンテナやコントロールが階層的に深々とネストされた構造になりがちです。ユーザーが操作する要素(例:Button)はシグナルを発生させますが、これらを適切に処理するためには、UIのルートノードにあるスクリプトにシグナルを接続する必要があります。

例としての設定例をご紹介します。

alt alt

Root要素のCenterContainerスクリプトには以下の関数が含まれており、任意のボタンが押されたときに呼び出したいです。

extends CenterContainer

func _on_button_pressed(button_name):
    print(button_name, " was pressed")

これらのボタンは Button シーンのインスタンスであり、動的にコードを実行してボタンテキストやその他のプロパティを設定できるオブジェクトを表しています。あるいは、ゲーム状態に応じてコンテナへ動的に追加・削除されるボタンがあるかもしれません。いずれにせよ、ボタンのシグナルを接続するために必要な手順は以下の通りです。

extends Button

func _ready():
    pressed.connect(owner._on_button_pressed.bind(name))

ボタンをツリー内のどこに配置して(コンテナを追加した場合でも)も、CenterContainerは常にownerとして機能します。

関連レシピ