Godot : _enter_tree()や、_ready()などの実行順をなるべく詳しく紹介
この記事の概要
この記事では、オープンソースのゲームエンジンであるGodot Engineで、ノード
と呼ばれるクラスの初期化時の処理について解説します。
この記事の対象者
-
_init()
や_ready()
、_process()
などの関数の役割がなんとなく分かっている方 - 「シグナルが想定したタイミングで実行されてない」と感じる方
- ノードの親子関係が複雑になるにつれて、どういう順番で処理されるのかが、いまいちわからない方
前提 : ノードが初期化される際の処理について
Godot Engineでは、Node系
のクラスが初期化される際には、いくつかの関数やシグナル、通知が呼び出されます。例えば、ノードがゲーム画面に表示されるタイミングや、スクリプト経由でnew()
やinstantiate()
によって初期化された瞬間、add_child()
でシーンツリーに加わった瞬間などです。
例えば以下のような関数・シグナルがあります:
_init()
_enter_tree()
_ready()
tree_entered シグナル
ready シグナル
-
_notification()
/ この記事では通知
と呼称します
これらの関数やシグナル、通知などの処理順について、いくつかの親子関係を想定し、なるべく網羅的に紹介します[1][2]。また、この記事では便宜上、シーンツリーに加えられて、_process()
が実行される直前までを「初期化」の範囲とします。
この記事を書く際に使ったコード
GDScript
のコードを末尾に記載しました。同じ結果になるか、ぜひご自分でもお試しください。
ノードがひとつの場合
まずは、ツリーに親子がない、孤立したノードがひとつの場合の実行順を紹介します。
_init()
NOTIFICATION_ENTER_CANVAS
NOTIFICATION_ENTER_TREE
_enter_tree()
tree_entered シグナル
_ready()
NOTIFICATION_READY
ready シグナル
_process()
上述の通り、_init()
→ _enter_tree()
→ _ready()
→ _process()
の順番で処理されるのが、初期化の際の基本的な流れとなります。
また、_enter_tree()
の後にtree_entered シグナル
が発火するなど、関数 → シグナルの順番に処理されることがわかると思います。
親子関係にあるノードの場合
次に、親子関係にある2つのノードの場合の実行順です。
- 親
_init()
- 子
_init()
- 親
_enter_tree()
- 子
_enter_tree()
- 子
_ready()
- 親
_ready()
- 親
_process()
- 子
_process()
通知とシグナルを含めた順番を表示
- 親
_init()
- 子
_init()
- 親
NOTIFICATION_ENTER_CANVAS
- 親
NOTIFICATION_ENTER_TREE
- 親
_enter_tree()
- 親
tree_entered シグナル
- 子
NOTIFICATION_ENTER_CANVAS
- 子
NOTIFICATION_ENTER_TREE
- 子
_enter_tree()
- 子
tree_entered シグナル
- 子
_ready()
- 子
NOTIFICATION_READY
- 子
ready シグナル
- 親
_ready()
- 親
NOTIFICATION_READY
- 親
ready シグナル
- 親
_process()
- 子
_process()
ノードツリーに加えられたことを知らせるenter_tree
系の処理までは、親→子の順番で、ready
系では子→親となり、_process
で再び親→子の順番に戻ることがわかりますね。
子が2つ以上ある場合
続いては、子ノードが2つある場合の挙動です。
- 親
_init()
- 子A
_init()
- 子B
_init()
- 親
_enter_tree()
- 子A
_enter_tree()
- 子B
_enter_tree()
- 子A
_ready()
- 子B
_ready()
- 親
_ready()
- 親
_process()
- 子A
_process()
- 子B
_process()
通知とシグナルを含めた順番を表示
- 親
_init()
- 子A
_init()
- 子B
_init()
- 親
NOTIFICATION_ENTER_CANVAS
- 親
NOTIFICATION_ENTER_TREE
- 親
_enter_tree()
- 親
tree_entered シグナル
- 子A
NOTIFICATION_ENTER_CANVAS
- 子A
NOTIFICATION_ENTER_TREE
- 子A
_enter_tree()
- 子A
tree_entered シグナル
- 子B
NOTIFICATION_ENTER_CANVAS
- 子B
NOTIFICATION_ENTER_TREE
- 子B
_enter_tree()
- 子B
tree_entered シグナル
- 子A
_ready()
- 子A
NOTIFICATION_READY
- 子A
ready シグナル
- 子B
_ready()
- 子B
NOTIFICATION_READY
- 子B
ready シグナル
- 親
_ready()
- 親
NOTIFICATION_READY
- 親
ready シグナル
- 親
_process()
- 子A
_process()
- 子B
_process()
基本的には、上述の親子関係にある場合と同様です。
ただ、子ノードが同階層に2つ以上ある場合は、常にシーンエディタで「上」にあるノードから順番に処理されることがわかります。
同じ階層のノードが処理される順番は常に「上」から
子ノード2つと、孫ノードがある場合
今度は、先ほどの子ノードのうち、「Child_A」に孫ノードを加えた場合の挙動を見てみましょう。
- 親
_init()
- 子A
_init()
- 孫
_init()
- 子B
_init()
- 親
_enter_tree()
- 子A
_enter_tree()
- 孫
_enter_tree()
- 子B
_enter_tree()
- 孫
_ready()
- 子A
_ready()
- 子B
_ready()
- 親
_ready()
- 親
_process()
- 子A
_process()
- 孫
_process()
- 子B
_process()
通知とシグナルを含めた順番を表示
- 親
_init()
- 子A
_init()
- 孫
_init()
- 子B
_init()
- 親
NOTIFICATION_ENTER_CANVAS
- 親
NOTIFICATION_ENTER_TREE
- 親
_enter_tree()
- 親
tree_entered シグナル
- 子A
NOTIFICATION_ENTER_CANVAS
- 子A
NOTIFICATION_ENTER_TREE
- 子A
_enter_tree()
- 子A
tree_entered シグナル
- 孫
NOTIFICATION_ENTER_CANVAS
- 孫
NOTIFICATION_ENTER_TREE
- 孫
_enter_tree()
- 孫
tree_entered シグナル
- 子B
NOTIFICATION_ENTER_CANVAS
- 子B
NOTIFICATION_ENTER_TREE
- 子B
_enter_tree()
- 子B
tree_entered シグナル
- 孫
_ready()
- 孫
NOTIFICATION_READY
- 孫
ready シグナル
- 子A
_ready()
- 子A
NOTIFICATION_READY
- 子A
ready シグナル
- 子B
_ready()
- 子B
NOTIFICATION_READY
- 子B
ready シグナル
- 親
_ready()
- 親
NOTIFICATION_READY
- 親
ready シグナル
- 親
_process()
- 子A
_process()
- 孫
_process()
- 子B
_process()
子A
の下に孫
ノードがある場合は、基本的に子B
の処理の前に孫
が処理されることがわかります。
また、_ready()
については、常にノードの一番深いところにあるノードが優先して処理されることがわかります(今回の場合は孫
ノード)。
まとめ
ポイント
親子関係にあるノードにおいては、以下の特徴を覚えておくと良いと思います。
-
ready系
は、ツリー上の一番深いノードの_ready()
から処理される - 同じ階層のノードは、基本的にシーンエディタで「上」に表示されているものから処理される
- 常に関数 → シグナルの順番で実行される(例:
_ready()
→ready シグナル
の順) - 通知の実行位置には、例えば「関数 → 通知 → シグナル」のような法則はない
- この記事とは関係ないですが、
@onready
修飾子の変数は_ready()
実行前に代入されます
飛び道具の"通知"
また、普段あまり使われることがない通知
/ _notification()
ですが、_enter_tree()
の前など、ピンポイントに「このタイミングで処理したい」みたいな処理に使うことが可能です。
とはいえ、乱用するとコードの見通しが悪くなる可能性もあるので注意が必要です。初期化周りの処理は、基本的に_init()
、_enter_tree()
、_ready()
で事足りると思います[3]。
おわり
ノードの初期化時の処理順は、最初はなかなか理解しづらいところが多いかもしれません。今回の記事が、その理解の手助けになるようであれば幸いです。
今回の記事で使ったコード
以下のコードを使っていただければ、ご自身の環境でも同様の順番になることが確認できると思います[4]。
extends Node
var is_process_started := false
func _init():
print("_init()")
self.ready.connect(func(): print(self.name + " ready シグナル"))
self.tree_entered.connect(func(): print(self.name + " tree_entered シグナル"))
func _enter_tree():
print(self.name + " _enter_tree()")
func _ready():
print(self.name + " _ready()")
func _process(delta):
if not is_process_started:
is_process_started = true
print(self.name + " _process()")
func _notification(what):
match what:
NOTIFICATION_ENTER_CANVAS:
print(self.name + " NOTIFICATION_ENTER_CANVAS")
NOTIFICATION_ENTER_TREE:
print(self.name + " NOTIFICATION_ENTER_TREE")
NOTIFICATION_READY:
print(self.name + " NOTIFICATION_READY")
-
この題材については、すでに国内外で様々な記事において多数紹介されていて、今更記事にする必要もないかと考えていました。が、
_notification()
を含めて紹介しているものがあまりないことや、学びはじめの方には少し理解しづらい点もあり、同じ題材でも複数の記事で多角的に扱うメリットを念頭に、あえて記事化することとしました。なお、この記事の元ネタは、_notification()
を含めたready
,enter_tree
などの実行順というタイトルのスクラップで書いたものとなります。 ↩︎ -
個人的には…ですが、
_init()
と_ready()
で事足りることが多いです。 ↩︎ -
self.name
を使うことで、ノード名をそのままログに出力できるようにしています。ただ、_init()
が実行される時点ではself.name
を参照できないため、そこだけself.name
を書いていません。コードは自由にお使いください。 ↩︎
Discussion