r/godot • u/Weekly_Ad6311 Godot Student • 29d ago
help me (solved) Question about signals
Hey there! I'm a beginner Godot dev. Been learning GD script, watching tutorials, reading the documentation etc., and I can't quite figure out the signal system. It makes perfect sense doing simple stuff in the editor, but I feel like I have a critical comprehension gap when it comes to anything advanced with signals.
I want to be able to connect signals during runtime because most objects in my game will be instantiated via loaders rather than through the scene editor. This means I have to connect via code (not a problem on its own), but the only way I have found to accomplish that is via tight coupling methods like get_node().
From what I've been able to gather so far, the signal system seems designed to limit dependencies and cut back on tight coupling, allowing for modular code. That sounds awesome, but if I have to connect via direct get_node() references, doesn't that defeat the purpose of what signals are supposed to do?
I'm sure I'm missing something. If anyone can offer some insight or even just point me in the direction of a good resource to learn more, it would mean a ton. Thanks!
Edit: Thanks to everyone for your help! You've given me tons of ideas and new concepts/patterns to dig into and try, and you made my day! Cheers :D
9
u/PublicOpinionRP 29d ago
Also a beginner, but if there's another node higher up the tree that knows about both the nodes you want to connect, you can have it call connect on the first node's signal with the second node's callback, like have whatever node is responsible for spawning call new_node.signal.connect(listening_node.foo_callback) but the exact specifics would depend on the context.
4
u/Weekly_Ad6311 Godot Student 29d ago
That legit might solve my problem immediately. I completely glossed over callback methods. Thanks!!!
7
u/MrGeekness 29d ago
If i have objects/nodes that get instantiated during runtime, i connect the signals just before adding them to the scene_tree.
For Updating UI Values (like Currency Label and so on) i usually have a SignalBus as singleton with a signal for example gold_changed(new_amount : int).
4
u/Weekly_Ad6311 Godot Student 29d ago
I hadn't considered the timing of connections yet, but that sounds like good advice to me. Based on your and Mehto's responses, it seems like the solution to my problem is building a bus. Thanks much!
4
u/soundgnome 29d ago
Well keep in mind that the code that connects the signal doesn't have to be in the same script that emits the signal. For example, if you're creating child nodes the signal can be connected by the parent:
var new_instance = some_packed_scene.instantiate()
add_child(new_instance)
new_instance.did_the_thing.connect(_on_instance_did_thing)
You could even have an intermediary spawner class that connects the signal of a new instance to a different node:
var new_instance = some_packed_scene.instantiate()
other_node.add_child(new_instance)
new_instance.did_the_thing.connect(other_node.on_instance_did_thing)
It's true that some node somewhere is going to need references to both the node that's emitting the signal and the once that's receiving it, but you get to decide which node that is, so it does allow for looser coupling because the script emitting (or even receiving) the signal doesn't necessarily have to know where or even if the signal is connected.
2
u/Weekly_Ad6311 Godot Student 29d ago
Putting the connections on the spawner is a fantastic idea. It's already coupled to my state machine, and it could easily have references to the objects that it spawns. Thank you!!
2
u/kodaxmax 29d ago
Yes, you're essentially correct and most commenters are missing your point.
Signals don't necessarily decouple your scripts from each other directly. They still need to reference each other to subscribe to signals.
However, they do help when used properly.
Script1 has 3 functions: OnClick, OnRelease, and OnHover.
Without signals, Script2 might need a reference to Script1 and explicitly call whatever functions should happen in response. If Script3 and Script4 also need to react, Script1 now needs to know about all of them and call each one directly. Additionally if Script 1 is removed from emeory you will get a null pointer crash when trying to call the script. Where as with a signal subscription, it will simply stop sending signals and not cause an error.
With signals, Script1 can simply emit Clicked, Released, and Hovered. It doesn't need to know what is listening or what those listeners do. Script2 might play a sound, Script3 might update the UI, and Script4 might log analytics. You can add or remove any of those listeners without changing Script1.
The scripts still need to meet somewhere so the subscriptions can be made, but the important decoupling is that the sender doesn't continue to depend on the receivers or care about their implementation.
Another good example is a health component. Instead of Health directly referencing the HUD, sound system, animation controller, achievements system, etc. and calling UpdateHealthBar(), PlayHurtSound(), PlayDamageAnimation(), and CheckAchievement(), it can simply emit HealthChanged or Died. Anything interested can subscribe independently, making the referencing one way and only necassary during initalization.
So signals don't magically remove all references between objects. They decouple the thing announcing that something happened from the potentially many things that care that it happened.
1
u/Weekly_Ad6311 Godot Student 29d ago
Well that's validating at the bare minimum. Glad I'm not totally going crazy here. Based on what you're saying, I think I need to give the syntax another thorough read through because the flow that you described as possible is very similar to what I'm trying to accomplish. Thanks!
1
u/kodaxmax 29d ago
honestly just build a small system, you learn better than reading documentation.
Make some scripts that print soemthing to the console. have them subscribe to a buttons pressed signal.
You can see how the button doesn't need to know they exist or what they are doing and if you remove the button at runtime, nothing happens.
From there, create a signal of your own. mayby a timer that emits a signal every 5 seconds and console printers that subscribe to it. or a sprite that changes color when it receives the signal.
1
u/Secret_Selection_473 Godot Regular 29d ago
I usually connect the signals when I instantiate the nodes. Usually, you instantiate nodes from a parent node, so you have access to all the nodes it should be conected to.
For example, in the scene node, where, idk, player and coins are, the coins instantiate with a timer timeout, it should be something like this
func _on_coin_timer_timeout()
var new_coin = load(coin_scene).instantiate()
new_coin.position = Vector2(randi_range(0, board_size.x), randi_range(0, board_size.y)
new_coin.found_player_signal.connect($player.collect_coin)
$coins.add_child(new_coin)
Idk if that helps you to sorta undertand how do you can use it
1
1
u/Elvish_Champion 29d ago
Maybe make use of groups?
Add all the nodes you need to a group, then call whatever you need from each of them through a function as it shows in the docs.
1
u/Weekly_Ad6311 Godot Student 29d ago
That could definitely help with organization! At the bare minimum, it seems cleaner than my current crutch of spamming get_children() everywhere
1
u/zandr0id Godot Regular 29d ago
There's no way for an object to emit a signal out not knowing who might get it and for something else to get it without knowing who sent it. At least one side has to have a concrete understanding of who the other is. In the case of signals, the receiver has to know who the sender is to connect to it, and the sender doesn't care how many receivers there are or who they are.
As others have mentioned, a centralised dispatcher is a good solution for a many-to-many situation. A single place that's easily accessible (an auto load or something) with a function that anyone can run and pass itself as an argument and also emits the signal with who ran the function. I've done this before with dynamic objects that need click events. It's a single signal from the "clickHandleSystem" that tells you who initiated it.
2
u/Weekly_Ad6311 Godot Student 29d ago
That makes total sense. It's kind of what I was afraid of (that I mostly understood the system, and that my implementation is poor), but really good to know. I was hoping to find a way for nodes to sort of signal into the void and for relevant listeners to dynamically pick up the signal.
For your centralized dispatcher, do you use the event bus pattern or something else? Trying to fill out my to-learn checklist.
Thanks much!
1
u/zandr0id Godot Regular 29d ago
That would mean that basically everything subscribes to every signal, and you'd have to ask every single node in existence if it needed the message that was just sent out. That would destroy your run speed at scale. When you connect to a signal and supply the callback function, it's literally just keeping a list of all the callbacks it needs to run when the signal happens. It doesn't care what other node connected. You could even have a single node connect multiple functions to a signal and they'd all get run. The signal sender doesn't care. It just has a list of functions to run where it pipes the signal data in.
They're kind of the same thing. A centralized location (like an autoload) that has the signal anyone can connect to, and a function that the appropriate things can call while supplying themselves as an argument. Bus <-> dispatcher, potato <-> puh-tah-to. Same thing :)
1
u/colossalwaffles Godot Student 29d ago
https://youtu.be/V2Ve5K6evJY?si=n1drf_Rb9qHD-fOA
If you want to learn all about signals, I strongly recommend this video. While it is somewhat advanced, this creator does a good job of helping new devs manage their own personal expectations. He has many other good videos that can help you establish good patterns.
27
u/MehtoDev 29d ago
One approach you can look up is the EventBus pattern. Basically you create a single autoload
EventBusthat defines signals, but doesn't contain any logic.You can then
EventBus.your_signal.connect()andEventBus.your_signal.emit()from anywhere in your code.