Is dynamically loading a script to add a `transform_changed` a huge anti pattern no go area?

Godot Version

4.4 and up

Context

  1. I want to make my nodes respond to a transformation change in a Bone2D
  2. I do not want developers to have to set a custom type (@tool) to their bones to make it work
  3. Because of this desire to make it easy for them, I use set_script to automatically ‘type cast’ the Bone2D instances.

Further reading on why.

Question

My question is: is this a massive anti-pattern that will have more unforeseen consequences than I foresee?

Will this forever tarnish my reputation? (don’t really have much of a reputation really)

Or can I just say: “Trust me, I know what I’m doing”

The tool I do not want to force upon my trusted early plugin adopters to have to look up themselves

@tool
extends Bone2D

class_name SVSBone2D

signal transform_changed(ref : SVSBone2D)

func _init() -> void:
	set_notify_transform(true)


func _notification(what: int) -> void:
	if what == NOTIFICATION_TRANSFORM_CHANGED:
		transform_changed.emit(self)

The evil anti-pattern I’m not afraid to use (yet)

(work in progress of course)

func _connect_to_bone_signals(current_bone_node : Node = null) -> void:
	if current_bone_node == null:
		current_bone_node = skeleton
	if current_bone_node is Bone2D and not current_bone_node is SVSBone2D:
		# NEVER USE EVAL! EVAL=EVIL! 
		# (except in game development of course)
		current_bone_node.set_script(load("res://addons/curved_lines_2d/svs_bone_2d.gd"))
	if current_bone_node is SVSBone2D:
		var bone = current_bone_node as SVSBone2D
		if not bone.transform_changed.is_connected(_on_bone_transform_changed):
			bone.transform_changed.connect(_on_bone_transform_changed)
	for bone in current_bone_node.get_children():
		if bone is Bone2D:
			_connect_to_bone_signals(bone)

I am aware of the potential performance impact of executing code on all these transforms… but in this case it is kind of the point: when a bone changes rotation, position or scale, the animated polygons (in my case curves) have to respond.

Quite nasty :slight_smile:

Connecting to bone’s draw signal might do the trick.

Thanks! it is noted, I will try that soon.

Draw signal works!

updated script:

func _connect_to_bone_signals(current_bone_node : Node = null) -> void:
	if current_bone_node == null:
		current_bone_node = skeleton
	#if current_bone_node is Bone2D and not current_bone_node is SVSBone2D:
		#current_bone_node.set_script(load("res://addons/curved_lines_2d/svs_bone_2d.gd"))
	if current_bone_node is Bone2D:
		var bone = current_bone_node as Bone2D
		if not bone.draw.is_connected(_on_bone_transform_changed):
			bone.draw.connect(_on_bone_transform_changed)
	for bone in current_bone_node.get_children():
		if bone is Bone2D:
			_connect_to_bone_signals(bone)

Just wondering now why invisible things emit draw, although it sort of makes sense to name it like that, but sounds like a case of: “we’re committed now, there is no way back”

And how to access the object that emitted any given signal. (like the spookily scoped javascript this arg)

You can bind the emitting bone to the function call:

			bone.draw.connect( _on_bone_transform_changed.bind(bone) )

I tried that actually, but I must have made a mistake, because my _on_bone_transform_changed(bone : Bone2D) callback was expecting a single argument bone… Should I have used _on_bone_transform_changed(bone : Object) ? Because the error message appeared to me as a argument count error, not a type error..

The type of the bound object should match the type declared in the handler (or its downstream type). For a custom signal you can just go typeless if you like.

If the issue is argument count, maybe some of the previous connections persist without the bound argument? What’s the exact error message, was the function called with 0 arguments?

Yes, because CanvasItem.draw has zero args, I assumed. Does bind override, or make an additional argument possible?

I usually use a lambda to add a var known in the connect’s scope, but I cannot disconnect that later..

..I strictly do not need to know the invoker, because I can check all the bones’ transforms for changes individually. That is not really expensive… But it does take another caching var to compare with.

But without extending Bone2D how do I create this custom signal?

You don’t need to.

It’s a runtime check against the handler function. The total number/type of additional arguments bound to the callable must match what you declared in the handler function. The default handler prototype doesn’t need to be strictly respected. You can add/bind arguments to it.

I will try again and report the exact error I ran into.

Maybe, just maybe, it was a code change after initialization of the connects, because I adapted code while the shape node already had a skeleton assigned in the scene.

Try this as a test:

@tool
extends CanvasItem

func _ready():
	draw.connect(foo1.bind("ARG1"))
	draw.connect(foo2.bind("ARG1", "ARG2"))
		
func foo1(arg1):
	print(arg1)
	
func foo2(arg1, arg2):
	print(arg1, " ", arg2)

I will.

But laundry first, trying to detach myself from the phone. Then finances. Then past bed time. Then act like I’m doing something important.
:grin:

What about: draw.connect(foo1.bind(self)) which comes closer to what I tried…?

Sure. You can bind anything. The engine shouldn’t complain if it matches the actual handler function the callable is pointing to.

You learn fast, apprentice.
:rofl:

Ok, yeah, now it works! Which is great, because I may need it later, but sadly in this specific case I reasoned I need to re-tessellate the whole curve anyway, no matter how many points were moved around. :person_shrugging: :grin:

Won’t need to do all that either.

Alas, connecting to the draw signal only works in the editor, because of course bones are not drawn at runtime (like I was worried about).

I can’t really think of any other signal to hook up to, but I will browse further. Do you have any ideas?

I have decided to go with this variant any way (unless something better comes up), to make it work at runtime:

svs_bone_2d.gd

extends Bone2D

func _init():
	set_notify_transform(true)


func _notification(what: int) -> void:
	if what == NOTIFICATION_TRANSFORM_CHANGED:
		draw.emit()

scalable_vector_shape_2d.gd

var _bone_transform_emit_script : Script = load("res://addons/curved_lines_2d/svs_bone_2d.gd")

func _ready():
	# (...) 
	if is_instance_valid(skeleton) and not Engine.is_editor_hint():
		for i in skeleton.get_bone_count():
			skeleton.get_bone(i).set_script(_bone_transform_emit_script)

I opened a pull request. You’re more than welcome to leave a scathing review there! :slight_smile:

Hm, couldn’t find a good alternative.

I’d still prefer manual checks every frame instead of forcing unsolicited scripts on user’s nodes. What to do if some of the bones already have scripts attached?

I implement _process already anyway, so it’s a fine option.

I hadn’t thought of it to be perfectly honest, actually.

Adding it to the PR review.

One might start to think that using signals for most of my plugin is an anti-pattern altogether.

Hooking them up and (dis)connecting them at all the editor life cycle stages next to runtime proved quite the hassle.