Node3D not found with Node.find_children(string)

Godot Version

4.4

Question

I have a node Enemy in my scene tree, which is not found.

get_node("/root/Game/Path").find_children("Enemy"):

It was spawned using this code:

func spawn_enemy(scene: PackedScene, tile: int):
	var enemy_instance = scene.instantiate()
	enemy_instance.name = "Enemy"
	enemy_instance.scene = scene
	# Add to tile node (e.g., tiles[2])
	self.tiles[tile].add_child(enemy_instance)

Other nodes can be found using find_children().

Hi! Could you upload a print of your scene tree?

find_children() isn’t recursive, and you’re calling add_child() on self.tiles[tile]. Where are you calling find_children() from in the hierarchy?

Hierarchy: root/Game/Path/Tile13/Enemy

yes it is recursive, I tested it with another Node “Mage” which was found. The difference was that I placed it manually in the scene tree in advance instead of spawning it at runtime like the “Enemy” node.

Sounds like you’re hitting a race condition where you are searching for the created enemy while it is still being created, or before you add it to the tree.

You haven’t shown enough code for us to help you.

Nice idea, but it is not a race condition. I have a button that triggers the find_children statement and I press it after I see the remote tree having the “Enemy” node.

extends Node3D
class_name Game

enum State {ENEMY_TURN, PLAYER_TURN, PLAYER_RUNNING, GAME_OVER, WIN}

@onready var path = $Path
@onready var player = path.find_child("Player")
@onready var boss = path.find_child("Mage")

var round = 0

var current_state

# Called when the node enters the scene tree for the first time.
func _ready():
	$Camera3D.set_current(true)
	self.player.new_turn()
	#TODO: animation
	self.current_state = State.PLAYER_TURN

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass


func _on_end_round_button_button_up() -> void:
	current_state = State.ENEMY_TURN
	self.enemy_turn()
	self.player.new_turn()
	current_state = State.PLAYER_TURN
	round += 1
	
func is_player_turn():
	return current_state == State.PLAYER_TURN
	
func enemy_turn():
	for enemy in get_node("/root/Game/Path").find_children("Enemy"): #FIXME
		path.move_enemy(enemy) #TODO: check if they don't block
	if boss.is_summoning_round(self.round) && path.tiles[13].get_node("Enemy") == null && path.tiles[13].get_node("Player") == null:
		path.spawn_enemy(preload("res://Scenes/skeleton.tscn"), 13)

extends Node3D

@onready var player = $Tile0/Player
var game
var player_selected = false
var tiles = {}
const NTILES = 32
var adjacency_matrix = [[1], [0,2,14], [1,3], [2,4], [3,5], [4,6], [5,7], [6,8], [7,9], [8,10], [9,11], [10,12], [11,13], [12, 30, 31], [1, 15], [14, 16], [15,17], [16,18], [17,19, 20], [18], [18,21], [20,22], [21,23], [22,24], [23,25], [24,26], [25,27], [26,28], [27,29], [28,30], [29, 13], [29]]
var path_matrix = []
var rng = RandomNumberGenerator.new()

# Called when the node enters the scene tree for the first time.
func _ready():
	for i in self.NTILES:
		self.tiles[i] = get_node("Tile" + str(i))
	calculate_paths()
	self.game = get_parent()

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

# extra_arg_0 is the tile number
func _on_area_3d_input_event(camera, event, position, normal, shape_idx, extra_arg_0):
	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and !event.pressed:
		if self.player_selected and extra_arg_0 in reachable_tiles(self.player.current_tile(), self.player.remaining_movement()):
			go_to_tile(extra_arg_0)


func reachable_tiles(position, steps):
	var r_tiles = []
	var current_positions = [position]
	while steps >= 0:
		var new_positions = []
		for current_position in current_positions:
			r_tiles.append(current_position)
			if !Utils.any(self.tiles[current_position].get_children(), "obstructs"):
			#if the square is not occupied, you can go to its neighbors
				for tile in self.adjacency_matrix[current_position]:
						if tile not in r_tiles:
							new_positions.append(tile)
		current_positions = new_positions
		steps -= 1
	return r_tiles

func unselect_player():
	for i in self.tiles:
		self.tiles[i].find_child("AnimationPlayer").play("unhighlight")
	self.player_selected = false
	
func calculate_paths():
	for i in self.NTILES:
		var row = []
		for j in self.NTILES:
			row.append([])
		self.path_matrix.append(row)
	for tile in self.NTILES:
		for neighbor in adjacency_matrix[tile]:
			self.path_matrix[tile][neighbor].append(neighbor)
	for k in self.NTILES:
		for i in self.NTILES:
			for j in self.NTILES:
				if !self.path_matrix[i][j].is_empty():
					var tile = self.path_matrix[i][j].back()
					for neighbor in adjacency_matrix[tile]:
						if neighbor != i:
							if self.path_matrix[i][neighbor].size() > (self.path_matrix[i][j] + [neighbor]).size() or self.path_matrix[i][neighbor].is_empty():
								self.path_matrix[i][neighbor] = self.path_matrix[i][j] + [neighbor]

func go_to_tile(tile):
	game.current_state = game.State.PLAYER_RUNNING
	unselect_player()
	self.player.get_node("AnimationPlayer").play("Running_A")
	var path = self.path_matrix[self.player.current_tile()][tile]
	var tween = self.get_tree().create_tween()
	var i = 0
	for next_tile in path:
		var target = self.tiles[next_tile].global_position + Vector3(0, 2, 0)
		var target_vector = -self.player.global_position.direction_to(target)
		GlobalAnimation.rotate_to_around_y(target_vector, self.player)
		var enemies = self.tiles[next_tile].get_children().filter(func(node): return node is Enemy)
		if enemies.size() > 0:
			#fight
			var prev_tile = self.tiles[path[i-1]]
			if i == 0:
				prev_tile = self.player.get_parent()
			var halfway_target_vector = 0.5*(target - prev_tile.global_position)
			self.player.get_node("AnimationPlayer").play("Running_A")
			tween.tween_property(self.player, "global_position", prev_tile.global_position + halfway_target_vector, 0.3)
			await tween.finished
			self.player.fight(enemies[0])
		var special_tiles = self.tiles[next_tile].get_children().filter(func(node): return node is GrassTile)
		for special_tile in special_tiles:
			special_tile.loot(self.player)
		tween.tween_property(self.player, "global_position", target, 0.3)
		i+=1
		self.player.move(1)
	await tween.finished #FIXME move after fight won
	self.player.get_parent().remove_child(self.player)
	self.tiles[tile].add_child(self.player)
	self.player.set_global_position(self.tiles[tile].global_position + Vector3(0, 2, 0))
	self.player.get_node("AnimationPlayer").play("Idle")
	game.current_state = game.State.PLAYER_TURN

func move_enemy(enemy: Node3D):
	#FIXME
	var path = enemy.move_path()
	var tween = self.get_tree().create_tween()
	for next_tile in path:
		var target = self.tiles[next_tile].global_position + Vector3(0, 2, 0)
		var target_vector = -enemy.global_position.direction_to(target)
		GlobalAnimation.rotate_to_around_y(target_vector, enemy)
		var enemies = self.tiles[next_tile].get_children().filter(func(node): return node is Enemy)
		if enemies == []:
			var player = self.tiles[next_tile].get_node("Player")
			if player:
				#fight
				var prev_tile = enemy.get_parent()
				var halfway_target_vector = 0.5*(target - prev_tile.global_position)
				enemy.get_node("AnimationPlayer").play("Running_A")
				tween.tween_property(enemy, "global_position", prev_tile.global_position + halfway_target_vector, 0.3)
				await tween.finished
				self.player.fight(enemy)
			#TODO: if trap, item, etc...
			tween.tween_property(enemy, "global_position", target, 0.3)
			await tween.finished
			enemy.get_parent().remove_child(enemy)
			self.tiles[next_tile].add_child(enemy)
			enemy.get_node("AnimationPlayer").play("Running_A")
			enemy.set_global_position(self.tiles[next_tile].global_position + Vector3(0, 2, 0))
	enemy.get_node("AnimationPlayer").play("Idle")

func spawn_enemy(scene: PackedScene, tile: int):
	var enemy_instance = scene.instantiate()
	enemy_instance.name = "Enemy"
	enemy_instance.scene = scene
	# Add to tile node (e.g., tiles[2])
	self.tiles[tile].add_child(enemy_instance)
	enemy_instance.find_child("AnimationPlayer").play("Idle")
	enemy_instance.global_transform.origin = self.tiles[tile].global_transform.origin + Vector3(0,2,0)


func _on_player_player_selected():
	if game.is_player_turn():
		self.player.get_node("AnimationPlayer").play("Jump_Full_Short")
		self.player_selected = true
		var reachable_tiles = reachable_tiles(self.player.current_tile(), self.player.remaining_movement())
		for tile in reachable_tiles:
			self.tiles[tile].find_child("AnimationPlayer").play("highlight") # animate to highlight
	
func _on_ui_gui_input(event):
	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and !event.pressed:
		unselect_player()

extends Node3D
class_name Enemy

@export var max_health: int
@export var current_health: int
@export var armor: int
@export var attack: int
@export var speed: int
@export var xp_reward: int
@export var status_effects: Array
@export var is_obstacle: bool
@export var scene: PackedScene

var path = [13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
var rng = RandomNumberGenerator.new()

#TODO
#@onready var health_bar = preload("res://Scenes/HealthBar.tscn").instantiate()

signal enemy_clicked(enemy)

func _ready() -> void:
	$Area3D.input_event.connect(_on_area_3d_input_event)
	self.enemy_clicked.connect(Callable(get_node("/root/Game/UI"), "_on_enemy_clicked"))
	#add_child(health_bar)
	self.get_node("AnimationPlayer").play("Idle")


func animate_damage(damage: int):
	var damage_label = load("res://Scenes/damage_riser.tscn")
	self.add_child(damage_label.instantiate())
	self.get_node("Node3D/Label3D").set_text(str(damage))
	self.get_node("Node3D").set_global_position(self.global_position + Vector3(0,8,0))
	self.get_node("Node3D/AnimationPlayer").play("rise")
	await self.get_node("Node3D/AnimationPlayer").animation_finished
	self.get_node("Node3D").queue_free()

func current_tile():
	return int(self.get_parent().name.substr(4))

func move_path():
	var index = self.path.find(self.current_tile())
	return path.slice(index + 1, index + 1 + self.speed)

func obstructs():
	return self.is_obstacle
	

func _on_area_3d_input_event(camera: Node, event: InputEvent, event_position: Vector3, normal: Vector3, shape_idx: int) -> void:
	if event is InputEventMouseButton and event.pressed:
		emit_signal("enemy_clicked", self)

Maybe the problem is that the Enemy class uses class_name? I need class_name for other script’s inheritances, though.

Ah, you’re right; it’s recursive unless you tell it not to be.