2

I have multiple exported variables in my script and anytime a single one is changed I want to invoke a common getter and let the values be set automatically

tool

export(float) var sample1 setget ,smthn_changed;
export(float) var sample2 setget ,smthn_changed;
export(float) var sample3 setget ,smthn_changed;

func smthn_changed():
    print("something changed!")

but this doesn't work and I have to create a setter for every single variable

is there any solution around this?

Theraot
  • 31,890
  • 5
  • 57
  • 86
cak3_lover
  • 1,440
  • 5
  • 26

1 Answers1

1

Please notice that you are defining smthn_changed as getter for those properties. And the getters are called when you try to read them, not when you try to assign them.

Alright, let us say you do want to know when the variables are being assigned. For that you would usually use setters, like this:

export var property:bool setget set_property

func set_property(new_value:bool) -> void:
    if property == new_value:
        return

    property = new_value
    print("value changed") # or emit a signal or whatever

The setter will be called at any time the variable is asignad externally (or internally with self.property = value, if you don't use self you can assign the variable directly without trigering the setter).

However, since you need to write the actual variable from the setter, this implies making a setter for each variable (if you used the same setter for multiple variable, you would not know which to set).


There is something else you can try: _set. The issue with _set is that will only be called for variables that are not declared in the script.

So, here is the plan:

  • We are going to declare backing variables with a different name, not export them.
  • We are going to use _set and _set to handle them.
  • And we are going to use _get_property_list to export them.

Let us see the case of just one variable:

tool
extends Spatial


var _x:String setget _no_set


func _set(property: String, value) -> bool:
    if property == "x":
        _x = value
        smth_changed()
        return true

    return false


func _get(property: String):
    if property == "x":
        return _x

    return null


func _get_property_list() -> Array:
    if not Engine.editor_hint or not is_inside_tree():
        return []

    return [
        {
            name = "x",
            type = TYPE_STRING,
            usage = PROPERTY_USAGE_DEFAULT
        }
    ]


func _no_set(_new_value) -> void:
    pass


func smth_changed() -> void:
    print("something changed!")

That is not worth the effort compared to a simple setter.

The setter _no_set is a setter that does nothing (not even set the variable). I have added it to prevent bypassing the mechanism externally by setting to the backing variable directly. You could add a warning there, as that is not something you code should be doing. On the flip the fact that your code should not be doing it could also be taken as an argument against having _no_set.

But let us see how it scales to multiple variables:

tool
extends Spatial


var _x:String setget _no_set
var _y:String setget _no_set


func _set(property: String, value) -> bool:
    match property:
        "x":
            _x = value
        "y":
            _y = value
        _:
            return false

    smth_changed()
    return true


func _get(property: String):
    match property:
        "x":
            return _x
        "y":
            return _y

    return null


func _get_property_list() -> Array:
    if not Engine.editor_hint or not is_inside_tree():
        return []

    return [
        {
            name = "x",
            type = TYPE_STRING,
            usage = PROPERTY_USAGE_DEFAULT
        },
        {
            name = "y",
            type = TYPE_STRING,
            usage = PROPERTY_USAGE_DEFAULT
        }
    ]


func _no_set(_new_value) -> void:
    pass


func smth_changed() -> void:
    print("something changed!")

Still not great, since we are having to repeat the variables multiple times. I would still prefer to have multiple setters, even if they all have the same code.


A generic case for an arbitrary set of properties is tricky, because calling get from _get, or set from _set, or get_property_list form _get_property_list in such way that it causes a stack overflow will crash Godot (and continue crashing it upon opening the project). So be careful when writing this code.

What I'm going to do to avoid calling get_property_list from _get_property_list is to put the properties we want in a dictionary:

tool
extends Spatial


var _properties := {
    "x": "",
    "y": ""
} setget _no_set, _no_get


func _set(property: String, value) -> bool:
    if _properties.has(property):
        _properties[property] = value
        smth_changed()
        return true

    return false


func _get(property: String):
    if _properties.has(property):
        return _properties[property]

    return null


func _get_property_list() -> Array:
    if not Engine.editor_hint or not is_inside_tree():
        return []

    var result := []
    for property_name in _properties.keys():
        result.append(
            {
                name = property_name,
                type = typeof(_properties[property_name]),
                usage = PROPERTY_USAGE_DEFAULT
            }
        )

    return result


func _no_set(_new_value) -> void:
    pass


func _no_get():
    return null


func smth_changed() -> void:
    print("something changed!")

Notice also that I'm reporting the type based on the value with typeof.

I'll leave it to you to decide if this approach is worth the effort. It might be, if the set of variables can change, for example. And I remind you that you can call property_list_changed_notify so that Godot calls _get_property_list and updates the inspector panel with the new set of properties.

Despite the _no_set, the dictionary could still be read and manipulated externally. So I added a getter _no_get that returns null to prevent that. If you like a warning in your _no_set, you may want a warning in your _no_get too.


Addendum: Here is a variation that uses an array for the names of the properties you want to export. This way you can still have regular variables instead of dealing with a dictionary. It is up to you to keep the array up to date.

tool
extends Spatial


var _property_names := ["x", "y"] setget _no_set, _no_get
var _x:String
var _y:String


func _set(property: String, value) -> bool:
    if _property_names.has(property):
        set("_" + property, value)
        smth_changed()
        return true

    return false


func _get(property: String):
    if _property_names.has(property):
        return get("_" + property)

    return null


func _get_property_list() -> Array:
    if not Engine.editor_hint or not is_inside_tree():
        return []

    var result := []
    for property_name in _property_names:
        if not "_" + property_name in self:
            push_warning("Not existing variable: " + property_name)
            continue

        result.append(
            {
                name = property_name,
                type = typeof(get("_" + property_name)),
                usage = PROPERTY_USAGE_DEFAULT
            }
        )

    return result


func _no_set(_new_value) -> void:
    pass


func _no_get():
    return null


func smth_changed() -> void:
    print("something changed!")

Note that I have added a check to prevent exporting without a backing variable, which also pushes a warning. It is not catastrophic to expose them as they would just be handled as null.

Also note that I had to remove the _no_set from the variables in this version. The reason being that I set them with set, which results in calling the setter, and since the _no_set didn't set the variable the result was it wasn't saving the values.


Addendum on resetting the value

If you want to add that arrow to reset the value you need to implement a couple of (yikes) undocumented methods:

func property_can_revert(property:String) -> bool:
    if property in self:
        return true

    return false


func property_get_revert(property:String):
    match typeof(get(property)):
        TYPE_NIL:
            return null
        TYPE_BOOL:
            return false
        TYPE_INT:
            return 0
        TYPE_REAL:
            return 0.0
        TYPE_STRING:
            return ""
        TYPE_VECTOR2:
            return Vector2()
        TYPE_RECT2:
            return Rect2()
        TYPE_VECTOR3:
            return Vector3()
        TYPE_TRANSFORM2D:
            return Transform2D()
        TYPE_PLANE:
            return Plane()
        TYPE_QUAT:
            return Quat()
        TYPE_AABB:
            return AABB()
        TYPE_BASIS:
            return Basis()
        TYPE_TRANSFORM:
            return Transform()
        TYPE_COLOR:
            return Color()
        TYPE_NODE_PATH:
            return NodePath()
        TYPE_RID:
            return RID(Object())
        TYPE_OBJECT:
            return Object()
        TYPE_DICTIONARY:
            return {}
        TYPE_ARRAY:
            return []
        TYPE_RAW_ARRAY:
            return PoolByteArray()
        TYPE_INT_ARRAY:
            return PoolIntArray()
        TYPE_REAL_ARRAY:
            return PoolRealArray()
        TYPE_STRING_ARRAY:
            return PoolStringArray()
        TYPE_VECTOR2_ARRAY:
            return PoolVector2Array()
        TYPE_VECTOR3_ARRAY:
            return PoolVector3Array()
        TYPE_COLOR_ARRAY:
            return PoolColorArray()

    return null

The idea is that property_can_revert will return true for any property that will have the reset arrow. And property_get_revert will give the value that will be set when you click said arrow. This had to be found in the source code since it is not documented.

Theraot
  • 31,890
  • 5
  • 57
  • 86
  • works like a charm but what if I want the type to be `export(float, -100,100,5)`? – cak3_lover Feb 17 '22 at 07:24
  • Also there seems to be no undo arrow with the exported variables – cak3_lover Feb 17 '22 at 07:31
  • 1
    @cakelover About stuff like `export(float, -100,100,5)`, aside from `name`, `type` and `usage` which I show here, you need a `hint` and `hint_string` for that. See [_get_property_list](https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-method-get-property-list) and [property hint](https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-propertyhint). I don't know how to add the reset arrow. – Theraot Feb 17 '22 at 08:11
  • @cakelover I found how to do the arrow, added it to the answer. – Theraot Feb 17 '22 at 08:25
  • How do I add `export(Array,float,0, 100,10) var Multiples=[0,0,0,0]`? I can't seem to find the hint or hint_string for it – cak3_lover Feb 18 '22 at 11:21
  • Also is it possible to export a variable during run time via this method? – cak3_lover Feb 18 '22 at 11:25
  • @cakelover Exporting variables is to have them appear in the inspector panel, and when the game is running there is no inspector panel to show exported variables. So that part is a no. Aside form that, `_set` and `_get` would work the same regardless if it is in the editor, so that part is a yes. On that note, I remind that you can access variables of other scripts even if they are not exported. – Theraot Feb 18 '22 at 12:17
  • @cakelover In fact, I was adding `_no_get` and `_no_set` in this answer to prevent other scripts from bypassing `_set`, although it is not perfect. In particular in the version that uses an array with the names of the variables, I could not put a `_no_set` on the variables or prevent them from being set, because that approach uses `set` to write them. And thus, other scripts could also access them directly, bypassing `_set`. By the way, if you access the variables with `self`, they should go through `set` too, that should make using the dictionary version easier. If only a little inconvenient. – Theraot Feb 18 '22 at 12:20
  • I meant what do I `append({name = property_name,type = typeof(Array),usage = PROPERTY_USAGE_DEFAULT,hint=???,hint_string=???,}` there doesn't seem to be any `hint` or `hint_string` for arrays – cak3_lover Feb 18 '22 at 12:25
  • @cakelover Turns out the answer is longer than I expected. Would you mind posting that as a separate question? – Theraot Feb 18 '22 at 13:25
  • take a look https://stackoverflow.com/q/71175503/18005234 – cak3_lover Feb 18 '22 at 14:56
  • I'm trying to access the `_properties` variable from another script , like this: `$"Prop_Node"._properties` but it keeps returning null, I want to print the dictionary `_properties` in the other script – cak3_lover Feb 24 '22 at 09:59
  • @cakelover See `_no_get`. `_no_get` and `_no_set` are there to prevent external manipulation. Such as, I don't know, being able to set the variables without triggering the changed notification. I believe I mentioned in the answer. Anyway, that is the reasoning behind it, but it is your project, modify it if you need to. Edit: as alternative, I suggest to make method that does the manipulation, and then you call that method from the other script. I believe that if you know that you only ever modify that variable from the same script it is easier to reason about it. – Theraot Feb 24 '22 at 10:09
  • what if it's only for reading and not writing to the value of `_properties`? – cak3_lover Feb 24 '22 at 10:11
  • 1
    @cakelover `_properties` is an object that can be modified without setting it. It is ultimately up to your discipline not to modify it. Similarly, it is up to your discipline to not allow the modification in the first place. After all, you can always change the code. My bias is towards what has less cognitive load, and what means less effort in the long run, even if it means more effort in the short term. I'm lazy. Thus I prefer to automate, and, I prefer the system stopping me from doing what I don't want to do, instead of being up to my will power. But your might prefer a different approach. – Theraot Feb 24 '22 at 10:17
  • @cakelover Ok, here is another alternative: If you want other script to be able to read `_properties`, but not modify it, then have a method that gives them a duplicate. Edit: And, yes, that can be the getter. – Theraot Feb 24 '22 at 10:20
  • Great answer! I would like the developer to choose a value through a combo of pre-defined values, like "`Wall`", "`Empty`", "`Marble`", etc.. and I would use this through an internal dictionary, is it possible? – Olivier Pons Aug 07 '22 at 09:10
  • @OlivierPons It is possible to make an enum with `_get_property_list`, you can post a question. I have answered a more complex version of it (an array of enum) at https://stackoverflow.com/a/71175772/402022 which I don't know if it is useful to you. – Theraot Aug 07 '22 at 09:38