During scene load, Godot will follow this execution order (new version):
- Allocate the new node,
all variables are zeroed. regular variables (no onready
) are initialized here to their default value (if they are export
, the value is overwritten on step 3, custom setter or not), zeroed if no default value is specified.
- Call
_init
on it.
- Set its properties (initialize
export
variables, and any custom setters run).
- If there are nodes that should be children, yet to do steps 1 to 3, follow this same steps for them.
- IDE signal connections are made (this happens after all nodes have gone though steps 1 to 3, and yes, that includes connections from and to future children).
- Add the node to its parent.
NOTIFICATION_PARENTED
(18).
NOTIFICATION_ENTER_TREE
(10).
- Send
tree_entered
signal.
- If there are nodes, that should be children, yet to do steps 6 to 9, follow this same steps for them.
NOTIFICATION_POST_ENTER_TREE
(27).
- Initialize any
onready
variables.
- Call
_ready
on it.
NOTIFICATION_READY
(13).
- Send
ready
signal.
One important caveat is that NOTIFICATION_ENTER_TREE
and tree_entered
happen if the parent is in the SceneTree
(e.g. if the node is created from script and yet to be added), also they do not happen in the editor (for tool
scripts). Speaking of tool
scripts, the steps from 11 on do not happen in the editor. Basically ready
and enter_tree
don't work on the editor.
See also:
Thus, when your setters call update
(step 3 above), this line is yet to run (it will run on step 12):
onready var coll = get_node("static/collision")
Because of that, coll
is null Nil
at:
coll.Shape = shape # or coll.shape the same error
And of course, Godot cannot access Shape
of Nil
.
A common solution is to follow this pattern:
func set_distance(new_distance):
distance = new_distance
if not is_inside_tree():
yield(self, "ready")
update()
This means that when the setter is called, but the node is not yet in the scene tree, Godot will halt the execution until it gets the ready
signal (which happens after the node is in the scene tree, and onready
variables are initialized).
However, there is another problem! This is a tool
script!
When running in the editor, onready
and ready
don't work, plus is_inside_tree
will return true
. Thus, in the editor, it will call update
but coll
is Nil
.
You can use Engine.editor_hint
to prevent the setters to call update
in the editor, like this:
func set_distance(new_distance):
distance = new_distance
if Engine.editor_hint:
return
if not is_inside_tree():
yield(self, "ready")
update()
Remember that for a tool
script you cannot rely on onready
. I suggest to use get_node_or_null
and check for null
. So you could do something like this:
func set_distance(new_distance):
distance = new_distance
coll = get_node_or_null("static/collision")
if coll == null:
return
update()
This way, when Godot calls the setter during initialization it will fail to find the node, and not call update
. You may also put that check inside update
, of course.
We can do a bit better. During runtime Godot will call the setter... So we will yield
to have the execution continue after ready
, at which point it should find the node, unless something has gone wrong.
func set_distance(new_distance):
distance = new_distance
if not Engine.editor_hint and not is_inside_tree():
yield(self, "ready")
coll = get_node_or_null("static/collision")
if coll == null:
return
update()
We might notify if something has gone wrong:
func set_distance(new_distance):
distance = new_distance
if not Engine.editor_hint and not is_inside_tree():
yield(self, "ready")
coll = get_node_or_null("static/collision")
if coll == null:
if not Engine.editor_hint:
push_error("static/collision not found")
return
update()
Extract to another function:
func set_distance(new_distance):
distance = new_distance
if not Engine.editor_hint and not is_inside_tree():
yield(self, "ready")
if can_update():
update()
func can_update() -> bool:
coll = get_node_or_null("static/collision")
if coll == null:
if not Engine.editor_hint:
push_error("static/collision not found")
return false
return true
You may reuse that extracted function in all your setters in the same pattern. Adapt it to whatever make sense on the script at hand. Here it only cares about the node that you use in update
, but you can make it whatever logic you need. And, yes, you could also do it inside update
proper.
There is something that has become somewhat a common practice: Exporting a bool variable to work as an update button.
We can do that with the pattern I described above:
export (bool) var refresh:bool setget set_refresh
func set_refresh(new_value):
refresh = new_value
if not Engine.editor_hint and not is_inside_tree():
yield(self, "ready")
if can_update():
update()
refresh = false
That will add a Refresh
atribute on the inspector panel, with a checkbox that you can click. Resulting in a call to update
if possible. Plus, the checkbox remains unchecked. This refresh
is always false.
The idea being that you can click it in the editor to call update
if needed.
Additionally, this setter will also execute during runtime, and call update
soon after initialization. You can have it only do something on the editor:
export (bool) var refresh:bool setget set_refresh
func set_refresh(new_value):
refresh = new_value
if Engine.editor_hint and can_update():
update()
refresh = false
You can add _get_configuration_warning
to provide a warning that will appear in the scene panel (Similar to how a PhysicsBody
tells you it needs a CollisionShape
or CollisionPolygon
):
func _get_configuration_warning():
if can_update():
return ""
return "static/collision not found"