How to generally detect a change of any property of Node?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By monnef

I know Resource has changed signal (which seems to be buggy in custom resources), but I am interested in prop change detection in Node. I tried overriding _set, but that didn’t pan out (I have to provide a return value signalling whether property exists, to add to the fire there is also no _set in the parent class, so I would have to somewhere store all property names which defeats the point).

Real world example:

export(SpawnShape) var shape_type:= SpawnShape.POINT
export var shape_rectangle_size:= Vector2(1, 1)
export var shape_circle_radius:= 1.0
export var shape_ellipse_radius:= Vector2(2, 1)

among other properties and I want to detect any change (in this specific case from inspector) in any property which name is starting with shape_.

This “solution” is verbose, mind-numbing, prone to bugs and doesn’t scale well:

export(SpawnShape) var shape_type:= SpawnShape.POINT setget _set_shape_type
export var shape_rectangle_size:= Vector2(1, 1) setget _set_shape_rectangle_size
export var shape_circle_radius:= 1.0 setget _set_shape_circle_radius
export var shape_ellipse_radius:= Vector2(2, 1) setget _set_shape_ellipse_radius

func _set_shape_type(x):
	shape_type = x
	update()

func _set_shape_rectangle_size(x):
	shape_rectangle_size = x
	update()

func _set_shape_circle_radius(x):
	shape_circle_radius = x
	update()

func _set_shape_ellipse_radius(x):
	shape_ellipse_radius = x
	update()
:bust_in_silhouette: Reply From: DDoop

Are you sure there is no _set() in parent class? That’s declared in Object, and I thought everything inherited that.

You can get all property names (including ones you designed, also exported variables) from a node by calling get_property_list(). This returns a big set of dictionaries with a lot of metadata for each property.
(One would extract the names like this:)

func extract_property_names_from_property_list(long_list_of_dictionaries):
	var names := []
	for item in long_list_of_dictionaries:
		names.append(item.name);
	return names

I wrote a custom _set() to help answer this question, observing the manipulated property list to handle the expected behavior of _set() while also extending it:

func _set(property: String, value) -> bool:
	if !(property in property_list): # property_list is a global variable 
		print("property %s is not in property_list" % property)
		return false
	else:
		print("emitting property changed signal now")
		emit_signal("PropertyChanged", property, value);
		return true

FWIW, this is my signal declaration:
signal PropertyChanged(property, value)

This gives me a way to emit signals when I call _set(); but only when _set() is explicitly called, i.e. scale *= 2.0 does not call it or emit the signal. This isn’t great because _set() necessitates passing strings around instead of property references, so Godot has to go do lookups based on what strings you pass it.