How to instantiate in a tool script?

Godot Version

4.3 beta1

Question

When I create a child node with this code they appear in the scene tree. However, after I save this scene and then reopen it, the child node is invisible both in the scene tree and to get_child_count() (it reports 0), and the code attempts to create another child node, leading to another node named “Test” being created.

func generate_nodes():
	if get_child_count() > 0:
		return
	var instance = Node2D.new()
	instance.name = "Test"
	add_child(instance)
	instance.owner = get_tree().edited_scene_root

How do I properly instantiate nodes so that they are visible in the scene tree when saved?

By calling the bare add_child, you are adding the child node to the script itself… I guess that’s not what you want? I’m not sure if setting the node owner is enough.

I’d expect something like this:

if get_tree().edited_scene_root.get_child_count() >= 0:
    return
# ...
get_tree().edited_scene_root.add_child(instance)

Thanks for the reply! I am trying to add them as children of the node that holds the script. I am not sure how to do this.

Please post your scene tree and some more context with the generate_nodes() script.

I tried the code you posted and it works as expected.

My scene tree consists of:
World
->Player
->TilemapManager
---->Tilemap 0
---->Tilemap 1
---->Tilemap 2

The nodes I am talking about are tilemaps which I am trying to manage using the TilemapManager. I am not able to add them as children of TilemapManager, which holds the script.

Here’s my code:

@tool
extends Node2D

@export_group("Tilemap Properties")
@export var layer_count: int:
	set(value):
		layer_count = value
		generate_tilemaps()

@export var base_layer: PackedScene = null:
	set(value):
		base_layer = value
		generate_tilemaps()
		
func generate_tilemaps():
	if base_layer == null:
		print("Base layer is undefined!")
		return
	var missing_layers = layer_count - get_child_count()
	if missing_layers > 0:
		for i in range(layer_count - missing_layers, layer_count, 1):
			var instance = base_layer.instantiate() 
			instance.name = "Layer " + str(i)
			instance.z_index = i

			add_child(instance)
			instance.owner = get_tree().edited_scene_root
			set_editable_instance(instance, true)
	elif missing_layers < 0:
		for i in range(-missing_layers):
			var child_to_remove = get_children()[-1]
			remove_child(child_to_remove)
			child_to_remove.queue_free()
1 Like

Alright, this is what happens when you find a stranger in the alps.

The issue

When TilemapManager is initialized by Godot it sets layer_count and base_layer before it is added to a scene tree. Since these properties call generate_tilemaps() which checks if it has children, which it doesn’t it’s just a lone node at this point, it semi-happily instantiates new layers and adds them as children. get_tree() fails with error data.tree is null. Setting owner fails because it is not in a scene tree.

Then after TilemapManager is added to the edited scene tree, Godot continues initializing the scene and attempts to add the nodes that were actually saved, but now cannot because of they clash with the nodes discussed in the previous paragraph.

The solush

You must defer the call to generate_tilemaps() until the edited scene is fully initialized. Something like the following pattern:

@export var layer_count: int:
	set(value):
		layer_count = value
		generate_tilemaps.call_deferred()

@export var base_layer: PackedScene = null:
	set(value):
		base_layer = value
		generate_tilemaps.call_deferred()

will work for this use case. There are many other ways to go about this.

1 Like

Little tip for future posts: include exact error messages that are printed in the output panel and the debugger panel. They help a lot!

Have a good one!

1 Like