godot-proposals icon indicating copy to clipboard operation
godot-proposals copied to clipboard

GDScript: Add instantiable and disposable `Signal` type.

Open SysError99 opened this issue 1 year ago • 4 comments

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.

SysError99 avatar Jan 31 '24 12:01 SysError99

Documentation:

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.

dalexeev avatar Feb 01 '24 07:02 dalexeev

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.

theraot avatar Feb 10 '24 16:02 theraot

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

radiantgurl avatar Apr 28 '24 22:04 radiantgurl

My PR was exactly for these types of things. But it more or less had in mind more of multi-threading than this.

radiantgurl avatar Apr 28 '24 23:04 radiantgurl

@RadiantUwU This proposal was made before https://github.com/godotengine/godot/pull/90674 was made.

SysError99 avatar May 11 '24 17:05 SysError99