Last active
April 13, 2025 10:04
-
-
Save thygrrr/2468d3d5fb1b0720a35d9dc02ce44e41 to your computer and use it in GitHub Desktop.
Awaiting multiple Signals in Godot 4.4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# SPDX-License-Identifier: Unlicense | |
extends Control | |
# Awaits until any one of the provided array of signals was emitted, and returns that signal | |
func any(signals : Array[Signal]) -> Signal: | |
var emitted : Array[Signal] = [] | |
var lambdas : Array[Callable] = [] | |
for sig in signals: | |
var lambda := func(): | |
emitted.append(sig) | |
sig.connect(lambda) | |
lambdas.append(lambda) | |
while emitted.is_empty(): | |
await get_tree().process_frame | |
for i in range(len(signals)): | |
signals[i].disconnect(lambdas[i]) | |
return emitted[0] | |
func _ready() -> void: | |
var interaction : Signal = await any([$Yes.pressed, $No.pressed]) | |
prints(interaction, "Yes?", interaction == $Yes.pressed, "No?", interaction == $No.pressed) | |
# To use this, create a UI scene (Control) containing two buttons, "Yes" and "No", and attach this script to it. | |
# This is also a typical use case (in addition to "game objectives") - acting on Dialogs and Menu Choices. | |
# The advantage of this approach is that the child scene doesn't need its signals wired or exposed to the parent/client node. | |
# Only exactly 1 button interaction is recorded, exactly once, and only for the buttons this menu cares for. | |
# It can never return an unexpected value, such as null, however there are some implications when queue_free() is called. | |
# This code in _ready can be adapted to be put into an execute() method that returns a strongly typed "Dialog Result". |
You know, something along these lines, perhaps.
class TaskResult:
var sig : Signal
var val : Variant
static func create(s : Signal, v : Variant) -> TaskResult:
var result = new()
result.sig = s;
result.val = v;
return result
func any_value(signals : Array[Signal]) -> TaskResult:
var emitted : Array[Signal] = []
var lambdas : Array[Callable] = []
var values : Array[Variant] = []
for sig in signals:
var lambda1 = func(value):
emitted.append(sig)
values.append(value)
sig.connect(lambda1)
lambdas.append(lambda1)
while emitted.is_empty():
await get_tree().process_frame
for i in range(len(signals)):
signals[i].disconnect(lambdas[i])
return TaskResult.create(emitted[0], values[0])
Godot's type system falls apart super hard here, so be careful, you can't stop here - this is Variant-Country.
It is also possible to match over the result, which is elegant, but sadly strictly requires fully typed variables (not nodes).
func _ready() -> void:
var interaction : Signal = await any([$Yes.pressed, $No.pressed])
var yes : Button = $Yes
var no : Button = $No
match interaction:
yes.pressed:
print("Yes!")
no.pressed:
print("No!")
What might be done is to change the original method to not return the signal, but rather the node it's on.
Edit: Unfortunately, no. You will always need an intermediary variable, even if directly working with Nodes or Objects.
func _ready() -> void:
var yes : Button = $Yes
var no : Button = $No
var interaction : Node = await any_node([yes.pressed, no.pressed])
match interaction:
yes:
print("Yes!")
no:
print("No!")
(any_node
returns sig.get_object()
instead of sig
)
Generally, I would say the fine-grained control over the verbs that are associated with the signals is more useful than the brevity of directly using the nodes.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
As an exercise to the reader:
all
signalsThe last one is annoying because GDscript has no user-exposed concept of variadic arguments, nor proper tuples, otherwise this would be quite elegant and flexible to implement. Think "5±2" and YAGNI.