When are export values set?

Desired behavior

I have a custom Control node with a Label as its child. I have many instances of the Control node in my UI in and I would like each to have different text in its child Label.

Solution 1

One way of doing this would be to enable Editable Children for the Control node, but since I have many instances of the node (and Label is nested far in the child nodes) I would like the Control node to change the Label text without cluttering the scene tree with editable children.

Solution 2

class_name CustomControl
extends Control

@export var label_text: String

func _ready():
    $.../Label.text = label_text

A solution to this would be to have an export variable label_text and then change Label.text in the _ready() function. The problem with doing it like this is that changes to the exported label_text variable do not show up in editor.

Solution 3

@tool
class_name CustomControl
extends Control

@export var label_text: String:
    set(new_text):
        label_text = new_text
        update_label()

func update_label():
    $.../Label.text = label_text

I can use the @tool annotation to update the Label in the setter of the exported variable. The problem with this approach is that the code will not work if you run the scene, as the setter will be called before the Control node’s children are initialized. This can be fixed by adding a check in the update_label function

func update_label():
    var label = get_node_or_null(".../Label")
    if label:
        $.../Label.text = label_text

which produces the desired behavior.

Question

I’m mostly wondering why this code works and if there is a better way of achieving the desired behavior. The thing I’m most confused about is when the setter of the exported variable is being called in node initialization? From my testing it seems that the initialization sequence is

  1. _init is called
  2. set is called the first time (node has no children)
  3. set is called the second time (node has children)
  4. node enters tree
  5. _ready is called

but I cannot find any official documentation telling me when setters of exported variables are called. Will the second call of set always occur after the children of the node have been initialized?

1 Like

See Exported value assignment

1 Like

I would do solution 3 with the tool and necessary code since exports should happen after init. You can set it, once all the inits have been called. Init goes top down, ready come bottom up.

If it tries to access the child node before, and after it inits, then just make a check to see if it exists before trying to set.

this is what i do

@tool
extends Node3D

@export_enum("h1:0","h2:1") var house := 0 :
	set(h):
		house = h
		if Engine.is_editor_hint() and is_node_ready():
			self.set_house()

func _ready():
	set_house()

func set_house():
  # set child node values
1 Like

Thank you very much. This seems like exactly what I was looking for. One last thing I still don’t understand is why does the setter get called twice when I change the default value of the exported text?

For example, if I have set label_text to "foo" in the editor of my CustomControl scene and I add the CustomControl as a child of some Control node in my UI scene and change the label_text to "bar" the setter gets called twice. Using the debugger I can see that the first time it sets label_text from blank to "foo" and the second time it sets label_text from "foo" to "bar". Why does it initialize with the default value first before changing to the exported value?

My only guess is the default scene has a saved export value “foo”. And the new parent scene has its own export value “bar”.

The parent scene will init the child. After the child finishes init, it will set “foo” for its export value (the default for the saved scene), and once the parent scene finishes init will set “bar” as it’s export value.

You can probably see these values saved in the .tscn files if you open them in a text editor.

If this is true, it does seem inefficient.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.