Godot version:
3.0.6
Issue description:
It is possible to yield for a function to complete using yield(func_name(), "completed"), as long as this function uses a yield instruction itself, as it returns a GDScriptFunctionState object that has a "completed" signal (with the "script_changed" signal being its only other due to inheriting from Object.) While it is functional, it would be easier if it were assumed that yielding on a GDScriptFunctionState object listens for the "completed" signal.
As it stands (using a real world example with an autotyping text box):
yield(box.begin(), "completed")
yield(box.read_line("This is my line of text."), "completed")
yield(box.wait_for_input(), "completed")
yield(box.read_line("This is my last line of text."), "completed")
yield(box.wait_for_input(true), "completed")
var result = yield(box.end(), "completed")
How it could be:
yield(box.begin())
yield(box.read_line("This is my line of text."))
yield(box.wait_for_input())
yield(box.read_line("This is my last line of text."))
yield(box.wait_for_input(true))
var result = yield(box.end())
Or possibly:
yield box.begin()
yield box.wait_for_input()
...
var result = yield box.end()
This would solve two issues:
First, the vast majority of Godot users I've encountered don't seem to know that it is possible to yield on a function to complete, as it's not well understood what the GDScriptFunctionState object does; while most understand that you can yield on a node and on a signal from that node, they don't realize the object received by yielding can be yielded on as well. By inferring the "completed" signal, yielding on a function will seem to 'just work', making it easier and more intuitive for beginners and adepts alike. Naive implementations will lead to filling a script with signals to indicate when a function has completed, and then yielding on that instead, emit_signal("func_ended") manually at the end of the function.
Second, even assuming you're among the elites who do know about the signal in GDScriptFunctionState, it's less tedious and less error-prone; GDScript does not autocomplete this signal, so you must either type "completed" out manually, or copy and paste it time and time again. Compared to C#'s await SomeAsyncFunc(), the current situation is not as nice to work with.
Edit: Thinking on it, maybe just having "completed" as the default parameter for the signal would be the better option.
it would be easier if it were assumed that yielding on a GDScriptFunctionState object listens for the "completed" signal.
The problem is that there's no way to know it is a GDScriptFunctionState until the code is running. So this is an extra runtime check and it will fail if the object is of any other type.
One simple but imperfect solution would be to implement a yieldc() which infers the "completed".
EDIT: Probably related https://github.com/godotengine/godot/issues/22947 In this issue await is suggested as the keyword.
(Also in response to @jkb0o , @OvermindDL1 and the referenced, but archived PR #23369 )
IMHO cramming three operations (generator-like yield, yielding until a certain signal is received, and async-esque await) into a single keyword creates more cognitive load then excessive keywords.
Their implementations are probably related (just genuinely curious and speculating, since I could not find any piece of documentation that would explain this part of godot): yield(object, signal) probably yields to some outer event loop that keeps track of returned function states, registers a callbacks that marks specific coroutines as "ready to be resumed" when signals fire - is this correct? If so, then yield(coro_handle, signal_name) means "add coro_handle to the list of coros to be resumed soon, and register a signal_name callback that would resume current coro"? Making all three cases syntactically indistinguishable make it much harder to comprehend. Also, curious since I could not find any docs mentioning it: who "owns" coroutines added to that list of tracked / soon resumed coroutines? In other words, who (and how) makes sure that coroutines (function states) that were instantiated in, let's say _ready() are freed/removed from the waiting queue when object is destroyed?
If one had the await keyword (as in PR #23369 ) or a built-in function, it would semantically separate signal waiting from yielding (as, for example, suggested/requested in #7618 and #7617 ) - and it would make perfect sense for this function to have "completed" as the default argument and be able to wait for other signals too.
await(timer(10), "timeout")
await($Button, "pressed")
await(play_animation("walk")) # implicit "completed" argument
it even reads like "await until timer timeouts, then until button pressed and until play animation completes" - and only requires creating an alias for yield with default second argument.
This still keeps async generators impossible to implement, I guess. The usecase for them is having "event streams" like
for button_press in button_press_stream():
pass
where button_press_stream itself is an async function that could pause/yield execution. Right now, I believe, this would be implemented like
while True:
button_press = yield($Button, "pressed")
...
but this loop might "loose" some of "pressed" signals if ... takes too long or itself yields. One can put a queue that would buffer all pressed signals on top of that, of course.
It seems like quite a few people really want some form of await-like functionality, since introducing await was the first usecase mentioned when compile-time macro were suggested (#10948).
Side note: the fact that (afaik, correct me if I am wrong) there are no APIs for synchronous code to add something to the async execution queue still seems somewhat weird to me coming from python. I mean something like ensure_future - there is currently no way to interface with async code without caller "becoming" async code. That is fairly weird.
@MInner I don't think this addresses #7617, which asks to make coroutines yielding to be handled differently by the calling code than functions returning.
Also, would yielding on the "completed" signal still allow manual resumption of the coroutine, through calling resume?
For the kind of behavior I'm looking for, you can also look at this comment https://github.com/godotengine/godot/issues/7617#issuecomment-386675499 and the API for Lua coroutines. https://www.lua.org/pil/9.1.html. Coroutine yielding (without waiting on a signal) shouldn't cause the function to return, it should give control to the closest enclosing coroutine-running call.
@raymoo oh, wow, I believe now I understand what you mean. IIUC, right now the only way for a coroutine to communicate with whoever started it is to emit is a signal. I.e. one can not just yield(thing) from a coroutine and hope that whoever created the coroutine would be able to receive it through the coroutine handle. Apparently, my speculation about how godot event loop works above was wrong, semantic of yield in gdscript differs quite a bit from other languages such as python, as there is just no yield(thing) operation period. Probably, if the coroutine had reference to itself then yield(obj, signal) would be semantically equivalent to obj.connect(signal, self, "resume"); yield() and that's it, no option to communicate with the caller using yield.
I mentioned it in more detail in the other thread, but while not being able to communicate upwards is an issue, #7617 is about not being able to use non-signal yield in a helper function, because there's no way to distinguish between a function call and the place you want to catch a yield at. In Lua for example, you have a separate coroutine.resume which will return if any of the code run inside it yields. In Lua yield does not make the enclosing function return.
Note: yield will be deprecated in 4.0
Feature and improvement proposals for the Godot Engine are now being discussed and reviewed in a dedicated Godot Improvement Proposals (GIP) (godotengine/godot-proposals) issue tracker. The GIP tracker has a detailed issue template designed so that proposals include all the relevant information to start a productive discussion and help the community assess the validity of the proposal for the engine.
The main (godotengine/godot) tracker is now solely dedicated to bug reports and Pull Requests, enabling contributors to have a better focus on bug fixing work. Therefore, we are now closing all older feature proposals on the main issue tracker.
If you are interested in this feature proposal, please open a new proposal on the GIP tracker following the given issue template (after checking that it doesn't exist already). Be sure to reference this closed issue if it includes any relevant discussion (which you are also encouraged to summarize in the new proposal). Thanks in advance!
@clayjohn Sorry to comment on an archived issue. Where can I see more information regarding this statement:
Note: yield will be deprecated in 4.0
I checked the issues labelled with the milestone 4.0 but haven't found anything.
Awesome, thanks! 鉂わ笍
Most helpful comment
One simple but imperfect solution would be to implement a
yieldc()which infers the "completed".EDIT: Probably related https://github.com/godotengine/godot/issues/22947 In this issue
awaitis suggested as the keyword.