Skip to content

Instantly share code, notes, and snippets.

@thygrrr
Last active April 13, 2025 10:04
Show Gist options
  • Save thygrrr/2468d3d5fb1b0720a35d9dc02ce44e41 to your computer and use it in GitHub Desktop.
Save thygrrr/2468d3d5fb1b0720a35d9dc02ce44e41 to your computer and use it in GitHub Desktop.
Awaiting multiple Signals in Godot 4.4
# 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".
@thygrrr
Copy link
Author

thygrrr commented Apr 12, 2025

As an exercise to the reader:

  • make a version of this that awaits all signals
  • make a version that takes a cancellation token of sorts. what will it return on external cancel?
  • make a version that returns values emitted by the signals

The 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.

@thygrrr
Copy link
Author

thygrrr commented Apr 12, 2025

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.

@thygrrr
Copy link
Author

thygrrr commented Apr 12, 2025

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