godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.14k stars 93 forks source link

Implement match-like syntax for await #4889

Open me2beats opened 2 years ago

me2beats commented 2 years ago

Describe the project you are working on

Games, plugins

Describe the problem or limitation you are having in your project

I want to listen to signal1 or signal2 once, the following way : if signal1 or signal2 is emitted, disconnect signal1 and signal2 then do the appropriate action (action1 or action2 which can be a block of code or a function)

I can do this

func _ready():
    connect(signal1, do_something1, [], CONNECT_ONCE)
    connect(signal2, do_something2, [], CONNECT_ONCE)

func do_something1():
    disconnect.signal2
    action1

func do_something2():
    disconnect.signal1
    action2

And this doesn't look nice (especially if you have do_something3 etc)

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Allowing match-like syntax would would make it easier

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

func _ready():
    await:
        signal1:
            action1
        signal2:
            action2

PS: The default/old syntax should still be available

await signal
action()

If this enhancement will not be used often, can it be worked around with a few lines of script?

See Describe the problem It is possible to use lambdas but the code still wouldn't be so clean

Is there a reason why this should be core and not an add-on in the asset library?

This is gdscript syntax feature

dalexeev commented 2 years ago

Is it a common use case that several different signals can be expected in the same place? Do not forget that signals have parameters, and for the same type of events it is better to use one signal with different parameters, rather than several different signals.

await is a very handy thing, but don't overuse it, for more complex situations, connect and a few separate functions are better (since the signal may never be emitted or the source may be removed before the receiver).

Also, if you have this corner case happening often, you can actually use the fact that Signals and Callables are first class objects:

await SignalMatcher.wait({
    signal1: func (): action1,
    signal2: func (): action2,
})
Zireael07 commented 2 years ago

Nice trick, should be documented somewhere @dalexeev

dalexeev commented 2 years ago

POC:

extends Node

signal s1()

signal s2()

class SignalMatcher:
    signal _signal_emitted(s: Signal)

    var _signals: Dictionary
    var _processed := false

    static func wait(signals: Dictionary) -> Signal:
        var sm := SignalMatcher.new(signals)
        return await sm._signal_emitted

    func _init(signals: Dictionary) -> void:
        _signals = signals
        for s in signals:
            s.connect(_emit_signal_emitted.bind(s))

    func _emit_signal_emitted(s: Signal) -> void:
        if not _processed:
            _signals[s].call()
            _processed = true
        _signal_emitted.emit(s)

func _ready() -> void:
    print('before')
    print(await SignalMatcher.wait({
        s1: func (): print(1),
        s2: func (): print(2),
    }))
    print('after')

func _input(event: InputEvent) -> void:
    if event.is_action_pressed('ui_left'):
        s1.emit()
    if event.is_action_pressed('ui_right'):
        s2.emit()

See also the comment. The methods Util.await_all and Util.await_any (returns the signal emitted first) make more sense.