I have a @tool script that, amongst other things, sets a label’s text once one of the exported properties is set:
@tool extends Control
# SU data
@export var su_name : String :
set(value):
if value:
su_name_label.text = value
Now, this is all good and for each instance of that Control, if I change su_name I also get the label updated accordingly.
The whole thing stops working the next time I open godot.
So as soon as it load the same scene, all labels would be empty and for each of them the following error would appear:
Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'Nil'.
Once the scene is opened, if I re-edit those, the labels would get correctly updated again.
Is there something in this logic that’s breaking at start up?
Your problem isn’t in the code you posted. It’s likely either in your _ready() function or if you’re loading data your load function. You’re encountering a race condition where you’re trying to set that value before the node is fully constructed.
To be clearer, if I manually drop another new instance of that tscn in there I don’t see any error, everything work as expected. It errors out the next time I open the project.
Now… is there any way for me to safely define that su_name_label variable once, then using it across my script, without having to change $su_name_label every time I change the hierarchy/naming of my scene?
Using the onready variable should only be a problem while in the editor.
Running the scene the variable will work.
Ostensibly, the only place it will be necessary to $ (shorthand for get_node()) is in the variable setter.
Using the onready variable should only be a problem while in the editor.
yep, but this is an editor tool, so if that variable fails every time I reopen the scene, then it won’t work.
To give you an idea: if I set that variable to ="foo" I see the label getting changed to “foo”.
Then the next time I open that scene in the editor, all labels will be blank.
If I replace this:
@onready var su_name_label:Label = $su_name_label
...
su_name_label.text = value
with this:
$su_name_label.text = value
it solves that problem, but now everywhere in the script where I want to change or read that label, I’ll have to manually input $su_name_label.somethingSomething, which means I’ll have to keep track and rebase every time I change the name or hierarchical position of the Label node in the scene.
that was my original test, which returns the following error
Parse Error: The default value is using "$" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. (Warning treated as error.)
Sorry, but this still gives me the same error.
I’m doing (I think) exactly what you’re suggesting
var su_name_label:Label
@export var su_name : String :
set(value):
if value:
su_name = value
su_name_label.text = value #"<---this is where it fails"
func _ready() -> void:
set_notify_transform(true)
ready.connect(_on_ready)
func _on_ready() -> void:
su_name_label = $su_name_label
When I reopen the scene (or reboot godot) I can see su_name being set to the value I had previously input, but the label is still blank, and: ERROR: res://editor/cell.gd:18 - Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'Nil'.
You are and aren’t doing what I’m suggesting. I forgot you are doing this assignment in a setter. The problem you are running into is a race condition. Specifically, you are setting the value of su_name before su_name_label has been fully initialized and created.
Most likely your problem is that since your variable is an exported variable, it’s running immediately because you’re assigning it a value in the editor. Here’s one solution. It’s hacky, but without knowing more about your program, I don’t have a better one.
var su_name_label:Label
var is_ready = false
@export var su_name : String :
set(value):
if value:
su_name = value
if is_ready:
su_name_label.text = value
func _ready() -> void:
set_notify_transform(true)
ready.connect(_on_ready)
func _on_ready() -> void:
is_ready = true
su_name_label = $su_name_label
su_name = su_name
What puzzles me is that this only happens when I reopen the scene again, the su_name variable is already set to the value I had assigned previously and therefore I assumed the setter wouldn’t even trigger.
So when dealing with @tool and exposed parameter, I assume there’s no way to defer the evaluation until the node is ready?
Something like @await_export or @export_on_ready
Whenever you reopen a scene with an @tool script, it is evaluated immediately because it is meant to be run in the editor.
You could split that variable into another scene that doesn’t have a tool script, or you have to deal with the fact that it’s differnt than a normal script.
Not to my knowledge. Which is again, why I gave you the code I did. It’s been the solution that has worked for me.
Typically, after implementing that solution, I usually find another way to deal with the problem down the road. Then I end up refactoring the implementation and the problem goes away on its own.
What further processing are you doing with that label in the context of @tool?
Once the program is running and the parent scene is ready you can access that label via the variable.
Be sure and store the value in the export variable.
@export var su_name : String :
set(value):
su_name = value
#Note that this if statement is of no use. A string (even "") won't be null and cannot be converted to a bool
if value:
Yes, tool scripts are definitely finicky. In testing I received multiple errors and then suddenly no errors.
(Remember to use reload saved scene under the scene menu. I don’t know the exact circumstance but sometimes tool scripts need this to begin working as expected.)
The end result is that the following code produces no errors:
@tool
extends Control
@export var su_name:String:
set(value):
su_name = value
#name = value # this line renames the node this code is in. I commented it out for testing
printt(name, value, "ssss", $Label)
$Label.text = value
It easy to reproduce the error you last quoted. If you try to change the name of a scene to an empty set “” then you get that error. So in your case obviously value is empty when you are assigning it to the name property.
Maybe giving the export var a default value will sidestep the error?
Or an if statement:
if value.length > 0:
name = value
I can think of no way to get around direct paths for your situation. But tool scripts are a bit of an unknown to me so maybe someone else can chime in with better solution.