First, I understand signals, mechanically, as in how to set up and use. And I recognize where they make sense (The example in the Godot docs of updating a health bar being a perfect use case). But I often see them being used or recommended in cases that seem unnecessary, which makes me wonder if either I’m missing some key benefit, or if people over use them simply because coupling is the devil.
An example:
I’ve created an interactable component with a callable that can be extended to define whatever functionality the object needs. When the player character detects the interactable and presses a button, the callable is called.
In addition, the player character has an on_interact(context) method. If needed, the interactable can call this method, passing over some context. Context in this case is the type of interactable, and a position. The character then decides if it should move to a position and play an animation (eg, move to the vehicle door and get in).
Now most of the examples I’ve seen if this type of system use signals. But why? It’s a closed system of which the only 2 involved parties will have the prerequisite methods. Why not call them directly, rather than jump through some extra hoops to connect and disconnect short lived signals?
The one place in this setup I can see a signal being useful is updating UI, where it’s always listening for a call to open a panel.
Say I have a treasure chest. If I write that chest in a closed system I have to recreate it each time, including how the player interacts with it.
In a modular system I can create that chest once, place it multiple times, and the player will know how to interact with each one. Signals give me a modular way to pass the information around - especially if I am communicating through a bus of some kind.
Further example - when an enemy dies you want to increment a counter. The death signal will likely be processed by something outside the current scene - something signals do very well.
You are right that, in a closed system, signals can be overkill - but in a modular system with multiple scenes they are a great way to pass data around.
It is a modular system. What I meant by closed is that only the character and interactable can interface with the system, and specifically, only the closest interactable object to the player.
I add the interactable component to an object (your treasure chest example), extend the interact callable on the parent script. Then I can make as many copies of the chest I want.
Typical advice is “signal up, call down” referring to signals going up the scene tree parents and calling functions on children. UI is a good starting place, a Button can signal up to another script and start one of their functions, the Button is reusable and no extended scripts needed. In my own game Interactables behave similar to buttons, only in 3D space. The interaction is always the same but by signaling up any other object can handle what to do (or not do) with that interaction.
These “components” usually benefit from signals, the signals let you drop in a scene and you can pick and choose which signals/functionality you want hooked up to other objects.
As components it can avoid inheritance issues too, let’s keep a treasure chest example, but now you also want an Interactable rigid body, like a rubber band ball. If you try to extend your Interactable it can’t also be a RigidBody. If your Interactable signals up the parent can be anything.
Okay. How do I know which chest I am interacting with?
With signals it doesn’t matter. The chest basically yells “this is what I have” and I listen to it.
If I have the player call a function on the chest I have to know which chest I am referencing so I can make the parent.method() call. There are certainly ways to identify the chest I am interacting with, but I can’t think of a solution simpler than listening for a signal.
I think one benefit is that it makes it easier to connect other things down the line. Like opening a chest, will it increment some counter somewhere or have some enemy or whatever react in a certain way? Then you can just connect them to the signal instead of have several function calls.
But if you decide to add some interaction like that you could adjust your current scripts easily.
First off, thanks everyone for the input. I’m going to push back a little, but not to be obstinate. I’m just trying to understand.
I don’t know the correct vocabulary, but I’m “extending” functionality not through inheritance, but through a callable. Below is the entire script for the interactable component:
class_name Interactable
extends Area3D
@export var interact_name: String = "[E]"
@export var is_interactable: bool = true
var interact: Callable = func():
pass
If I were to attach this to a treasure chest, I would then define/extend it on the parent script:
The interaction is initiated through Area3D hits (which of course, ironically, are one of the uses of signals I DO understand), so I’m already getting a reference to the interactables.
Here’s the Interaction component that goes on the player character:
extends Area3D
## Grants interaction ability to a character
@onready var interact_label: Label = $InteractLabel
var current_interactions: Array = []
var can_interact: bool = true
func _ready() -> void:
interact_label.hide()
area_entered.connect(_on_area_entered)
area_exited.connect(_on_area_exited)
## On button press, hide the prompt and call the interactable
func _input(event: InputEvent) -> void:
if event.is_action_pressed("interact") and can_interact:
if current_interactions:
can_interact = false
interact_label.hide()
await current_interactions[0].interact.call()
can_interact = true
## If active interactables are in range,
## sort them and display a prompt for the closest
func _process(_delta: float) -> void:
if current_interactions and can_interact:
current_interactions.sort_custom(_sort_by_nearest)
if current_interactions[0].is_interactable:
interact_label.text = current_interactions[0].interact_name
interact_label.show()
else:
interact_label.hide()
## return the closest interactable in range
func _sort_by_nearest(area1: Area3D, area2: Area3D):
var area1_dist = global_position.distance_squared_to(area1.global_position)
var area2_dist = global_position.distance_squared_to(area2.global_position)
return area1_dist < area2_dist
func _on_area_entered(area: Area3D) -> void:
current_interactions.push_back(area)
func _on_area_exited(area: Area3D) -> void:
current_interactions.erase(area)
Now that does make some sense. I was already seeing how I might need a signal for UI updates IN ADDITION to what I have, but I suppose 1 signal could make all the needed calls.
What you are programming is exactly how signals work. You are doing signals yourself, but slower and more error prone since it’s not native engine code.
Well then I stand corrected. I in fact do NOT understand how signals work, mechanically.
EDIT: But I will say that has put me in a better position to to see more places signals can be useful. I’m doing that exact pattern above in several places. Time to go back and make some changes.
Each signal is essentially an array of callables, when .emit() is used it loops through and calls every connected function. The flags help modify behavior too where needed, such as ONE_SHOT disconnecting after the emit and DEFERRED using .call_deferred instead of .call.
If you truly only have one character, one thing, that could possibly interact with the chest, then sure, you could do it without a signal.
But, lets have your character emit a signal when he gets hit. Then a function that updates the health bar could connect to the signal. And what if you want to keep track of how much fire damage you take, how much cold damage, etc over the life of the character, you could have a function connect to that signal. Then, what if you wanted to have an enemy that got powered up by the character taking damage, that enemy could connect to that signal…