godot
godot copied to clipboard
Exported variables do not call _set method
Godot version
3.4.stable
System information
Windows 10
Issue description
When I export a script property with export keyword or the _get_property_list method, this does not call _set method. In the case of using _get_property_list, if the property name does not match a script property, it calls the _set method, but it cannot set this property.
Steps to reproduce
I let a minimal project where there is a resource file with Number property.
- Open test.tres file in the inspector.
- For example, if Local To Scene property is changed, it is printed
changed. - If Number property is changed, it does nothing.
Minimal reproduction project
As far I tested, _set is only called if the script doesn't have a definition for that property (this means that you don't have var foo somewhere in the script)
I have the same issue on macOS (M1)
test.gd
tool
class_name Test
extends Resource
export var number = 0
func _set(property, value):
print("changed")
match property:
"Number":
number = value
_:
return false
return true
You match for Number with an uppercase N but your exported variable is number with a lowercase n. Is it intentional?
If you want a setter to be called for a property with export, you have to use setget.
No idea why _set would be called for some existing properties but not script vars though.
You match for
Numberwith an uppercaseNbut your exported variable isnumberwith a lowercasen. Is it intentional?
I read a tutorial, which did it like this, but I got the problem that it does not call the _set function. So I could not check if I should use snake_case there.
I do not know if this could get to work on some older version.
Can confirm the issue. Currently trying to make the script generate a texture every time one of 4 different variables is assigned. Having to write setget for all fo them is a hassle :[
AFAIK _set and _get only get called when accessing properties that do not exist as class members. I think this is similar to such "magic" methods of other languages too. And yeah, you either need to make your properties "virtual", not relying on an exported member, or you need to create setget boilerplate hell, as per traditions and customs. 🙃
Just ran into this and found a workaround, which is to add a / before your variable name:
"name": "/variable_name",
_set is then called every time the variable is updated.
It doesn't work in godot 4.0.2 as well
This is just my speculation.
After quick scan through the godot source code, here
https://github.com/godotengine/godot/blob/master/core/object/object.cpp#L253
It calls built-in setter (assuming this is where _set is called) after unable to set the script property.
The script property is either calling its bounded setter or directly write into the value if it is defined (without any setter https://github.com/godotengine/godot/blob/master/modules/gdscript/gdscript.cpp#L1499; Again my speculation).
So in godot 4, if you didn't give a setter directly. It will just by pass the built-in setter _set and directly write it into the value.
@YuriSizov AFAIK _set and _get only get called when accessing properties that do not exist as class members.
But what is then the reason for _set() to return a boolean? According to the docs it should return "false if the property should be handled normally", which only makes sense if there is a member to be handled normally.
@thornySoap Normally also means that if you don't override the method it is going to fail because that property doesn't exist. Docs can probably be improved.
@YuriSizov But on the other hand inherited class members (at least from built-in classes) do trigger _set(). Isn't that unintuitive?
If that's the case, then there is probably an issue with the multi-level call implementation for Object.set. Since each class in a hierarchy is considered in order, I guess it's possible that extending classes don't know about some parent members. I agree, this is a conflicting behavior, but I'd need to test it to see what's going on.
Is this still a valid issue?
this is happening on 4.x this issue also seems like its occurring for class objects as well, aka _set is not being called at all for class objects. aka when you define a class like this
class Fruit:
var amount = 0
the variables here aren't exported, though it seems like its still part of the issue
im bypassing this issue currently by attaching a setget to each variable, and call the _set function manually instead
I can confirm this on v4.2.2.stable.official [15073afe3]. The _set method is only called if there is no corresponding property, which seems to contradict the documentation.
By the looks of it this seems to be caused by the ordering in the GDScriptInstance class. Presumably, changing the order of these calls would fix the issue. If we can get an ok from a core maintainer, I'd be happy to implement the changes.
Here is a complete workaround example, using the return value of _set:
@export var test:int = 0:
set(value):
if _set("test", value):
test = value
func _set(property, value):
if property == "test":
if value < 10: # only allow values under 10
return true
return false
else:
return false
Btw, using _set is also necessary when wanting to override setter behaviour in a inheriting class, as it is not possible to override class members directly (and thus their setter).
I've tested this issue from 4.3, down to 4.1, and it's the same.
_set() and _get() aren't called for script variables, regardless their @export status.
set() still works properly for script variables, but _set() is not used.
Interestingly, set("c", true) runs, even if "c" does not appear in get_property_list(), which contradict's the _set() function's description:
Note that a property must be present in get_property_list(), otherwise this method will not be called.
Is this a valid issue or is more something related to documentation not being clear with _set/_get usage? What would be the normal behavior? Are script properties supposed to have to define their setter/getter and other script can override those with _set/_get usage?
Is this a valid issue or is more something related to documentation not being clear with
_set/_getusage? What would be the normal behavior? Are script properties supposed to have to define their setter/getter and other script can override those with_set/_getusage?
I would say this is a valid issue, _set() and _get()'s current behaviors (intentional or not) are too complicated, and has too many exceptions to be useful. The way I see it, _set() and _get() are supposed to be a way to tie into setters and getters, in scenarios where it's otherwise not an option (maybe you can't override a parent class' setter, as-is the case for built-in variables.)
Another, somewhat related issue with _set() is that it is context-based:
current = false will not cause _set() to run, however
self.current = false will cause _set() to run.
As far I tested,
_setis only called if the script doesn't have a definition for that property (this means that you don't havevar foosomewhere in the script)
Thanks, it worked like magic!
Doesn't this basically make _set() practically useless? For example, in my custom resource, all of the variables should call emit_changed() when they are changed, which would be extremely simple to do if _set() worked with script properties as well. Right now, however, I need to write a custom setter for each property, cluttering up my code. Here's an example comparison:
extends Resource
class_name MyCustomResource
@export var prop_1: String = "Hello World":
set(value):
prop_1 = value
emit_changed()
@export var prop_2: Mesh:
set(value):
prop_2 = value
setup_changed_signal(prop_2)
emit_changed()
@export var prop_3: Mesh:
set(value):
prop_3 = value
setup_changed_signal(prop_3)
emit_changed()
@export var prop_4: Mesh:
set(value):
prop_4 = value
setup_changed_signal(prop_4)
emit_changed()
@export var prop_5: Mesh:
set(value):
prop_5= value
setup_changed_signal(prop_5)
emit_changed()
func setup_changed_signal(res: Resource) -> void:
if res and not res.changed.is_connected(emit_changed):
res.changed.connect(emit_changed)
Compare to this. What makes this even better is that if I want to extend this resource, I wouldn't need to do all that boilerplate in the child class either. Great for modding:
extends Resource
class_name MyCustomResource
@export var u_prop_1: String = "Hello World"
@export var u_prop_2: Mesh
@export var u_prop_3: Mesh
@export var u_prop_4: Mesh
@export var u_prop_5: Mesh
func _set(property: StringName, value: Variant) -> bool:
if property.begins_with("u_"):
set(property, value)
if value is Resource and not value.changed.is_connected(emit_changed):
value.changed.connect(emit_changed)
emit_changed()
return true
If anything, I think that the first parameter should be the data along with the property name, like what is returned by Script.get_property_list(). Since the docs specifically state that the property must be present in the get_property_list(), this only makes sense. With that, the syntax would be even cleaner without the need to have custom naming schemes for the variables:
extends Resource
class_name MyCustomResource
@export var prop_1: String = "Hello World"
@export var prop_2: Mesh
@export var prop_3: Mesh
@export var prop_4: Mesh
@export var prop_5: Mesh
func _set(property: Dictionary, value: Variant) -> bool:
if property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
set(property.name, value)
if value is Resource and not value.changed.is_connected(emit_changed):
value.changed.connect(emit_changed)
emit_changed()
return true
Also see: https://github.com/godotengine/godot/issues/99828