Signals vs function calls for sibling objects - best choice with regards to readability?

I’m using a component system where each child node defines a particular action that can happen on that item, like so:

Where when RecievePhysicsEvents detects a physics collision, it should cause energy to increase and an animation to play.

There are two ways I can get these actions to trigger: I could have a signal on the RecievePhysicsEvents node that the other scripts listen to, or I could provide RecievePhysicsEvents with references to the other nodes and then run a function on them provided they are not null.

I get that Godot encourages liberal use of signals, my concern is it may make the code harder to read in some cases. Say I’m debugging something and I want to see everything that will be triggered after RecievePhysicsEvents detects its collision, there’s no easy way to do that just from looking at code. I assume the best way to check would be to run get_connections() on that signal but that just gives the script name not an object reference (let me know if there’s a better way). If I do function calls directly, it’s obvious at a glance what is and isn’t happening, and I can see what nodes are affected just by looking at the inspector. I also know exactly what the order of execution will be.

The other side is that if I use signals and only signals, it’s easy for me to see what causes e.g. energy to increase since I can just look at the connections that script makes. If I did function calls then I would need to use an external IDE to show me the useages of that function. But I have Rider and static type everything so this is trivial to do.

Thoughts?

1 Like

Signals should be connected in the base node (in this example, Item). You’d ideally avoid connecting child nodes to each other, and instead manage all connections and everything child nodes do in the base node (the parent).

The one thing you will hear most often in relation to signals is call down, signal up. That means any child nodes should signal to the parent when something happens, and the parents have the responsibility to react to that. In this particular case, Item will connect all child signals when ready:

# Item script

@onready var receive_physics_events_component: Node3D = $ReceivePhysicsEvents
@onready var increase_energy_component: Node3D = $IncreaseEnergy
@onready var start_animation_component: Node3D = $StartAnimation

func _ready() -> void:
    receive_physics_events_component.collision_detected.connect(_on_collision_detected)
    # Connect other signals here...

func _on_collision_detected() -> void:
    increase_energy_component.value += 50
    start_animation_component.start()
1 Like

Signal::get_connections() returns Callables. A Callable knows to which object it belongs.

Inter-sibling calling is fine imo. Just take care with situations where order of processing and/or initialization of nodes matter.