GDScript: Add instantiable and disposable `Signal` type.
Describe the project you are working on
Many of shortly-maintained Godot games.
Describe the problem or limitation you are having in your project
In many other programming languages that support async/await style of coroutine, the coroutine can be instantiated as wished and get automatically disposed right after the coroutine finishes. For example, in JavaScript, there's Promise that user can use to instantiate a coroutine that can be awaited on.
In Godot 4.x GDScript, it's not possible to create a disposable Signal like other programming languages. The snippet below doesn't work.
func _ready() -> void:
await coroutine_function() # Error connecting to signal: during await
print("test")
func coroutine_function() -> Signal:
var s := Signal()
_coroutine_function(s)
return s
func _coroutine_function(s: Signal) -> void:
await get_tree().create_timer(1.0).timeout
s.emit()
Describe the feature / enhancement and how it helps to overcome the problem or limitation
By the design, Godot supports adding custom Signals via Node::add_user_signal(), however, this type of signal stays permanently and cannot be removed (without https://github.com/godotengine/godot/pull/69243). Plus, making changes to the current implementation may probably need a complete overhaul of it. To workaround this issue, there may be another object type (let's call it DisposableSignal that extends RefCounted) to compensate for the limitation. In this way, the user can freely create disposable Signals, thus enhancing the programming language's usability without actually overhauling the entire design.
Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
The user will initalise a disposable signal with @GlobalScope::to_disposable_signal that takes a lambda that exposes the instance itself, and returns a signal resolved for the awaiter.
var lambda := func (s):
s.resolve("value")
await to_disposable_signal(lambda)
If this enhancement will not be used often, can it be worked around with a few lines of script?
While I don't believe this functionality will not be used often, it's relatively simple to create the implementation in GDScript with the snippet below:
extends RefCounted
class_name DisposableSignal
signal resolved(result)
static func create(lambda: Callable) -> Signal:
var disposable := DisposableSignal.new()
lambda.call_deferred(disposable)
return disposable.resolved
func resolve(result = null) -> void:
resolved.emit(result)
Is there a reason why this should be core and not an add-on in the asset library?
This is a feature that's available in so many general purpose programming languages. Since GDScript is now considered a general programming language, this functionality also enables more usability to the language itself.
Signal is a built-in Variant type that represents a signal of an Object instance.
Signal Signal ( )
Constructs an empty Signal with no object nor signal name bound.
Since Signal() is used to denote an empty/invalid signal, we should add some other constructor/static method/syntax for this feature if we accept it. Note that you cannot use methods like Object.get_incoming_connections() and Object.set_block_signals() with a signal alone.
This also raises the issue of memory management. In the current implementation we do not have a problem with it, since Signal only represents a signal of an object instance. When the object is deleted, its connections are also deleted. With this proposal we need to worry about reference counting or something like that.
Yes, but no. I agree Godot can use some kind of promise - in fact some people have made their own implementations, including myself - but a promise is not a signal.
By the way, my current implementation uses reference and unreference to make sure the promise stays alive as long as the task is not completed. It also supports failed and succeded states, and yes, that is states, which allows you to check if it finished before you got it, so you are not waiting on a signal forever. I'm not posting it here because it is multiple classes (to support different return types, all/any operators, and so on).
It would be intersting to have a common promise solution for the sake of interperability between addons that use promises.
See also:
- Recent: https://github.com/godotengine/godot-proposals/issues/8911
- Also https://github.com/godotengine/godot-proposals/issues/6243 - in particular: https://github.com/godotengine/godot-proposals/issues/6243#issuecomment-1420709631
- And notable: https://github.com/godotengine/godot-proposals/issues/5510
Note that Godot does not need promises per-se, this is mostly for game and addon developers.
Godot supports adding custom Signals via Node::add_user_signal(), however, this type of signal stays permanently and cannot be removed (without https://github.com/godotengine/godot/pull/69243).
Huh?? https://github.com/godotengine/godot/pull/90674
My PR was exactly for these types of things. But it more or less had in mind more of multi-threading than this.
@RadiantUwU This proposal was made before https://github.com/godotengine/godot/pull/90674 was made.