cak3_lover
cak3_lover

Reputation: 1958

How to create a getter function without a setter function?

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?

Upvotes: 2

Views: 2563

Answers (1)

Theraot
Theraot

Reputation: 40315

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.

Upvotes: 2

Related Questions