godot icon indicating copy to clipboard operation
godot copied to clipboard

Exported variables do not call _set method

Open pizcadesaber opened this issue 3 years ago • 18 comments

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.

  1. Open test.tres file in the inspector.
  2. For example, if Local To Scene property is changed, it is printed changed.
  3. If Number property is changed, it does nothing.

Minimal reproduction project

Test.zip

pizcadesaber avatar Jan 08 '22 03:01 pizcadesaber

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)

AnidemDex avatar Jan 11 '22 04:01 AnidemDex

I have the same issue on macOS (M1)

bram-dingelstad avatar Jan 20 '22 13:01 bram-dingelstad

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.

Zylann avatar Jan 20 '22 18:01 Zylann

You match for Number with an uppercase N but your exported variable is number with a lowercase n. 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.

pizcadesaber avatar Jan 20 '22 18:01 pizcadesaber

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 :[

QbieShay avatar May 25 '22 17:05 QbieShay

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

YuriSizov avatar May 25 '22 17:05 YuriSizov

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.

JayCee-101 avatar Aug 09 '22 20:08 JayCee-101

It doesn't work in godot 4.0.2 as well

RechieKho avatar May 16 '23 03:05 RechieKho

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.

RechieKho avatar May 16 '23 04:05 RechieKho

@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 avatar May 26 '23 18:05 thornySoap

@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 avatar May 26 '23 19:05 YuriSizov

@YuriSizov But on the other hand inherited class members (at least from built-in classes) do trigger _set(). Isn't that unintuitive?

thornySoap avatar May 27 '23 07:05 thornySoap

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.

YuriSizov avatar May 27 '23 12:05 YuriSizov

Is this still a valid issue?

AnidemDex avatar Oct 20 '23 20:10 AnidemDex

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

Nukiloco avatar May 19 '24 22:05 Nukiloco

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.

ghost avatar Jun 11 '24 21:06 ghost

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

Okxa avatar Jul 20 '24 09:07 Okxa

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.

image

addmix avatar Sep 24 '24 00:09 addmix

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?

AnidemDex avatar Sep 29 '24 14:09 AnidemDex

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?

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.

addmix avatar Nov 14 '24 04:11 addmix

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)

Thanks, it worked like magic!

Anderson-r-santos avatar Feb 08 '25 15:02 Anderson-r-santos

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

RusheNewman avatar Jun 04 '25 19:06 RusheNewman

Also see: https://github.com/godotengine/godot/issues/99828

RusheNewman avatar Jun 04 '25 19:06 RusheNewman