Godot version: 3.1.6
OS/device including version: Solus Gnome 4.0
Issue description:
Alright, this sounds kinda complex, but the issue is simple, so I'll give context first.
I have this script:
extends Node
export (NodePath) var node_path = ".."
onready var node = get_node(node_path)
export (bool) var enalbed = true setget set_enabled
func set_enabled(enable):
enabled = enable
node.set_process(enabled)
The thing is, if I change the Enabled
value in the inspector, the set_enabled
method crashes when running the scene, because seems like the method is called before the _ready
callback, or at least before the onready
variable being set. So it reports that node
is null, therefore it is unabled to call set_process
.
I think that this could be fixed by changing the order in which setget
methods are called, making them wait for idle
process at least?
Steps to reproduce:
Enabled
to falseThis has been the case since forever, it's not a bug. The rule is, properties of a node with USAGE_STORAGE
are always set right when it is deserialized for instancing. There is no simple way to defer this to the moment it is added to the tree: unless I'm missing something, you could do something like var scene = packed_scene.instance()
, but add_child()
might be called later, or never. This applies to every node, scripted, or builtin. It means you can't implement the setter like you did, you have to check if the node is in tree first, and initialize in _ready
as well. _ready
is the deferring mechanic.
So, basically, the rule is = don't use exported
and setget
for setter/getter methods that are instance dependant?
Wouldn't this work in that case?
func set_enabled(enable):
enabled = enable
yield(get_tree(), "idle_frame")
node.set_process(enabled)
@KoBeWi get_tree()
might return null since the node is not yet in the tree.
Ah, right. An ugly hack would be adding a Timer node and waiting for a very short timeout, but I doubt you'd need to do this THAT much.
@KoBeWi won't work either because Timer
works using _process
and _process
is not called when the node is not in the tree ¯\_(ツ)_/¯
Really all you can do in such case is set the variable, eventually modify the current node, but anything external to self
needs to wait for _ready()
or _enter_tree()
.
@KoBeWi
get_tree()
might return null since the node is not yet in the tree.
Exactly what happened.
func _ready():
node.set_process(enabled)
won't work either because Timer works using _process and _process is not called when the node is not in the tree ¯_(ツ)_/¯
Yeah, but it will work AFTER it's added to tree. The idea is basically to wait until everything is set up. set_process
is useless outside tree anyways.
EDIT:
Or what @santouits wrote plus this:
var ready
func _ready():
node.set_process(enabled)
ready = true
func set_enabled(enable):
enabled = enable
if ready:
node.set_process(enabled)
func _ready(): node.set_process(enabled)
This breaks the idea of the setter, since it is meant to keep both nodes in sync whenever one changed the other should change as well, not only when one is ready. I think that what I can do is to instead of setting this directly, connect a signal and emit this signal when this enabled
setter is called.
Basically my script is:
func set_enabled(enable):
.set_enabled(enable)
if enable:
emit_signal("started")
platform_actor.velocity = Vector2.ZERO
else:
emit_signal("finished")
So in my specific case I think I can connect the started
signal to the platform_actor.set_velocity
method passing Vector2.ZERO
as extra argument. But well, this is a very ad hoc solution if you asked me.
won't work either because Timer works using _process and _process is not called when the node is not in the tree ¯_(ツ)_/¯
Yeah, but it will work AFTER it's added to tree. The idea is basically to wait until everything is set up.
set_process
is useless outside tree anyways.EDIT:
Or what @santouits wrote plus this:var ready func _ready(): node.set_process(enabled) ready = true func set_enabled(enable): enabled = enable if ready: node.set_process(enabled)
There is the Node.is_inside_tree()
method for that, this fixed the problem by the way!
func set_enabled(enable):
.set_enabled(enable)
if not is_inside_tree():
return
if enable:
emit_signal("started")
platform_actor.velocity = Vector2.ZERO
else:
emit_signal("finished")
I usually do this in my scripts like so:
export var x = 5 setget set_x
func _ready():
set_x(x)
func set_x(new_x):
x = new_x
if is_inside_tree():
# do something, like:
$sprite.frame = x
Note that #6491 will help remove a few lines of that boilerplate, even more if it is made to work with onready
nicely.
For this I figured I should update such properties deferred most of the time:
extends Node
export (NodePath) var node_path = ".."
onready var node = get_node(node_path)
export (bool) var enabled = true setget set_enabled
func set_enabled(enable):
enabled = enable
call_deferred('_update_properties') # updated on idle (next) frame
func _update_properties():
node.set_process(enabled)
The above mechanism may cause _update_properties
called multiple times in a single frame. Here's more a sophisticated and optimized mechanism if you need to change properties that update often (picked this up from engine internals):
extends Node
export (NodePath) var node_path = ".."
onready var node = get_node(node_path)
export (bool) var enabled = true setget set_enabled
var _update_queued = false
func set_enabled(enable):
enabled = enable
_queue_update()
func _update_properties():
node.set_process(enabled)
# might as well do more stuff
_update_queued = false
func _queue_update():
if not is_inside_tree():
return
if _update_queued:
return
_update_queued = true
call_deferred("_update_properties")
What I always do is
if not is_inside_tree(): yield(self, 'ready')
This will delay the setters instead of returning and not setting anything at all.
Still I don't like typing it out every time.
Seems like this isn't a bug, just a general misunderstanding that is quite logical and the engine provides tools to achieve the desired behaviors quite simply.
I'll close it.
Most helpful comment
What I always do is
This will delay the setters instead of returning and not setting anything at all.
Still I don't like typing it out every time.