Godot4で子ノードをすべて取得する_ベンチマーク付

NodeNode の関数 get_children では、そのノードの直下のノードをすべて取得できます。

しかしこれは直下のみで、そのさらに子ノードは取得できません。

動的にノードツリーを構築したり、子がいくつあるか分からないが、すべて取得したい場合に必要になります。

あまり使用機会が少なく、そもそもコストが高いためあまり使いたくない処理ではあります。

そのため組み込み関数としては用意されていないのでしょう。

_process() 内などの頻繁に呼び出される関数で使うのは避けましょう……


この、「指定したノードの直下の子ノードだけではなく、すべての子ノードを取得する方法」をいくつか記述します。

おすすめは1の While を使うやり方です。いちばん速いので。

ベンチマークは雑なので参考程度に。


1.While

#1While
	var ary:Array = get_all_children_while(self) #usec=2122

func get_all_children_while(in_node):
	var waiting = in_node.get_children()
	var ary:Array = []
	while not waiting.is_empty():
		var node = waiting.pop_back()
		waiting.append_array(node.get_children())
		ary.append(node)
	return ary

引数に入れたノードのすべての子ノードが入った Array を返します。

While でノードの get_children() 結果が空になるまでループします。


2.再帰処理を使った関数を用意する

#2再帰
	var ary:Array = get_all_children_recursive(self) #usec=4309

func get_all_children_recursive(in_node, arr:=[]):
	arr.append(in_node)
	for child in in_node.get_children():
		arr = get_all_children_recursive(child,arr)
	return arr

引数に入れたノードと、そのすべての子ノードが入った Array を返します。

引数に入れたノードも入るので注意が必要です。


再帰処理の制限として、コールスタック最大数を超えるとエラーになってしまいます。

デフォルト値は1024ですが、2047まで増やせます

プロジェクト設定の debug/settings/gdscript/max_call_stack で設定します。


しかし、1.While より遅いです。


3. NodeNode の関数 find_children() を使用する

#3find_children
	var ary:Array = find_children("*","",true,false) #usec=33147

Array[Node] find_children(pattern: String, type: String = "", recursive: bool = true, owned: bool = true)

NodeNodefind_children 関数は、

指定したパターン文字列にマッチするノードを取得できる関数ですが、recursive:bool 引数に true を入れるとすべての子ノードを取得できるで、それを使ってみます。

マッチ条件は * でどのノードにもマッチさせます。


パターン文字列でマッチさせるという、そこそこ重い処理があるのもあって、とても遅いです。

パターンは役に立ちますが、すべてのノードがとりたいだけという目的にはあっていませんね。


4. ノードにグループを設定してシーンツリーの get_nodes_in_group() を使用

#4グループ
# グループを設定しておく、エディタのインスペクター → ノード → グループからも可能
var node = Node.new()
node.name = "Node" + str(i)
node.add_to_group(&"group")

var ary = get_tree().get_nodes_in_group(&"group") #usec=99068

少し逸れますが、シーンツリーとグループの機能を使うこともできます。

ノードにグループを設定して、シーンツリーからグループ名を指定して get_nodes_in_group を呼ぶと

ツリー上にある指定したグループ名のグループのノードがすべて取得できます。

Array[Node] get_nodes_in_group(group: StringName)


ただし、他の方法とは違いシーンツリーを対象にするため、

子ノードではなくツリー上の同じグループのノードが取得されます。


いちばん遅いです。これは予想外でした。


シーンツリー&グループは、よく使われているイメージでしたが、意外と遅いですね……

グループを使わずにAutoload のシングルトンのノードのプロパティとかに DictionaryArray を用意して、

そこにデータを格納して外から参照・操作させるほうがパフォーマンス良いし便利なのでは……?


結論

extends Node

class_name MyStatic

static func get_all_children(in_node):
	var children = in_node.get_children()
	var ary:Array = []
	while not children.is_empty():
		var node = children.pop_back()
		children.append_array(node.get_children())
		ary.append(node)
	return ary

Autoload にこんな感じの static 関数を用意しておけば良さそうです。


試したコードまとめ

extends Node

func _ready():
	var n = self
	for i in 1000:
		var node = Node.new()
		node.name = "Node" + str(i)
		node.add_to_group(&"group")
		n.add_child(node)
		var node2 = Node.new()
		node2.add_to_group(&"group")
		n.add_child(node2)
		n = node

	var a = Time.get_ticks_usec()

#1While
#	var ary:Array = get_all_children_while(self) #usec=2122
#2再帰
#	var ary:Array = get_all_children_recursive(self) #usec=4309
#3find_children
#	var ary:Array = find_children("*","",true,false) #usec=33147
#4グループ
	var ary = get_tree().get_nodes_in_group(&"group") #usec=99068

	print(ary.size())
	var b = Time.get_ticks_usec()
	print(b - a)

func get_all_children_recursive(in_node, arr:=[]):
	arr.append(in_node)
	for child in in_node.get_children():
		arr = get_all_children_recursive(child,arr)
	return arr

func get_all_children_while(in_node):
	var waiting = in_node.get_children()
	var ary:Array = []
	while not waiting.is_empty():
		var node = waiting.pop_back()
		waiting.append_array(node.get_children())
		ary.append(node)
	return ary

参考

Godot Engine - Q&A How to get ALL children from a node?