Godot: Exported variables break setget methods that are "ready" dependent

Created on 9 Jul 2019  Â·  15Comments  Â·  Source: godotengine/godot

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:

  1. Create a Node
  2. Add a child Node
  3. In the child Node add a script
  4. Copy paste the script above mentioned
  5. In the inspector set Enabled to false
  6. Test the scene
archived discussion gdscript

Most helpful comment

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.

All 15 comments

This 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.

Was this page helpful?
0 / 5 - 0 ratings