Reputation: 1958
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
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:
_set
and _set
to handle them._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