Null reference on direct child in tools script

Godot Version

4.5

Question

I have a scene which is a simple static object with a sprite2d and a collision shape. I then attach a tools script to the scene in order to allow me to randomize automatically on load, randomize manually in editor, or manually set the sprite for that object. The code is:

@tool
extends StaticBody2D

@export_range(0,3,1) var size: int:
	set(value):
		size = value
		$Sprite2D.frame_coords = Vector2i(size, style)

@export_enum("Bush", "Rock") var style: int:
	set(value):
		style = value
		$Sprite2D.frame_coords = Vector2i(size, style)

@export var random: bool

func _ready() -> void:
	if random:
		size = randi_range(0, $Sprite2D.hframes - 1)
		style = [0,1].pick_random()
	$Sprite2D.frame_coords = Vector2i(size, style)

The code works perfectly in the editor, allowing me to see the sprite get updated in real time when I change the values. Running the game, however, causes it to crash because its getting a null reference on the $Sprite2D node.
This only happens in the set(value) code though. If i comment that out, the same exact code in the ready function works fine and doesn’t cause a null ref.

I’m pretty sure this is just an issue of the code trying to access Sprite2D too soon, but I don’t really understand why. The tutorial I’m following says this is an issue with @tools and all you have to do is to reload scenes and soft reload script, but this seems to only work randomly, and I have to close and reopen scenes multiple times every time I add unrelated code to that script for some reason.

I don’t understand why a direct child of the node the script is attached to would need time to be loaded. It’s not loaded in code and it’s not instanced, its literally just its own sprite.

Can anyone explain to me what exactly is going on and, if possible, how I can retain the instant updating in the editor while also not having the game crash on me 9 times out of 10? I’m mostly just interested in why this happens since I find it really confusing.

When instancing a node, values of exported variables get applied during object initialization, which is before the node (and its children) are ready. Thus you can’t access child nodes at that time.

If you are setting $Sprite2D.frame_coords in _ready() anyways, only change it in the setter functions if the node is ready:

	set(value):
		size = value
		if is_node_ready():
			$Sprite2D.frame_coords = Vector2i(size, style)
1 Like

Works flawlessly, thank you very much.