Godot version:
Godot 3.0.6
Issue description:
It would be awesome if it was possible to batch coroutines and yield on all of them.
Example:
func _ready():
print("Hello")
test_1()
test_2()
# Both functions should be done before printing world.
# Can I somehow just yield for both of them even though I don't know which one completes first?
print("World")
func test_1():
yield(get_tree().create_timer(rand_range(0, 10)), "timeout")
func test_2():
yield(get_tree().create_timer(rand_range(0, 10)), "timeout")
I could do yield(test_1(), "completed")
But I don't know which test function will finish last, so I have no idea which one I should yield on.
Also I can't do:
yield(test_1(), "completed")
yield(test_2(), "completed")
That requires that test_1 is completed before test_2
Let me know your thoughts on this. Discuss! :smile:
I agree that coroutines in GDScript should have available batching functionality similar to Javascript's Promises or Python's Asyncio.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
https://docs.python.org/3/library/asyncio-task.html#asyncio.gather
I thought about how this problem is solved best in GDScript as of now.
This is the easiest way I found so far:
TL;DR: Add the _GDScriptFunctionState (http://docs.godotengine.org/en/3.0/classes/class_gdscriptfunctionstate.html)_ to a list when calling a coroutine and then run through the list waiting for each specific coroutine that is still valid.
extends Node
func _ready():
# var time_spent = OS.get_ticks_msec()
print("HELLO")
var coroutine_list = []
coroutine_list.append(delay_1())
coroutine_list.append(delay_2())
for coroutine in coroutine_list:
if coroutine.is_valid():
yield(coroutine, "completed")
print("WORLD")
# time_spent = OS.get_ticks_msec() - time_spent
# print("Time Spent: " + str(time_spent / 1000.0))
func delay_1():
randomize()
var time = rand_range(5, 20)
print("'delay_1()' waiting: ", time, "s")
yield(get_tree().create_timer(time), "timeout")
func delay_2():
randomize()
var time = rand_range(5, 20)
print("'delay_2()' waiting: ", time, "s")
yield(get_tree().create_timer(time), "timeout")
For awaiting the first signal and last signal in a set, I have a workaround like this (singleton coroutines.gd
):
extends Node
static func await_any_signal(args):
assert(len(args) % 2 == 0)
var emitter = _Emitter.new()
for i in range(0, len(args), 2):
var object = args[i]
var signal_name = args[i + 1]
object.connect(signal_name, emitter, "emit")
yield(emitter, 'emitted')
static func await_all_signals(args):
assert(len(args) % 2 == 0)
var emitter = _Emitter.new()
for i in range(0, len(args), 2):
var object = args[i]
var signal_name = args[i + 1]
object.connect(signal_name, emitter, 'emit')
#warning-ignore:unused_variable
for i in range(0, len(args), 2):
yield(emitter, 'emitted')
class _Emitter:
signal emitted
func emit():
emit_signal("emitted")
Usage:
yield(coroutines.await_any_signal([object1, "signal1", object2, "signal2"]), "completed")
yield(coroutines.await_all_signals([object1, "signal1", object2, "signal2"]), "completed")
It's not pretty but it works.
A minor but possibly useful extension of the code posted above:
static func select(specs):
var emitter = _Emitter.new()
for triplet in specs:
match triplet:
[var object, var signal_name, var retval]:
object.connect(signal_name, emitter, 'emit', [retval])
[var object, var signal_name]:
object.connect(signal_name, emitter, 'emit', [object])
return yield(emitter, 'emitted')
class _Emitter:
signal emitted(object)
func emit(object):
emit_signal("emitted", object)
# ...
var result = yield(select([
[$Button, "pressed", "button1"],
[$Button2, "pressed", "button2"]
]), "completed")
print(result) # prints button1 or button2
I had the same issue, and here the solution I came up with:
Promise.gd
is autoloaded and is used as a singleton, I got my inspiration from javascript
await Promise.all(promises)
# Promise.gd
extends Node
func resolve(coroutine: GDScriptFunctionState):
var valid_count = 0
if coroutine.is_valid():
valid_count += 1
yield(coroutine, "completed")
if valid_count == 0:
yield(get_tree(), "idle_frame")
func all(coroutines: Array):
var valid_count = 0
print(coroutines)
for coroutine in coroutines:
if coroutine.is_valid():
valid_count += 1
yield(coroutine, "completed")
if valid_count == 0:
yield(get_tree(), "idle_frame")
And here an usage example:
awaiting all coroutines to finish
var coroutines = []
for item in items:
coroutines.push_back(item.some_func())
# wait for all coroutines to finish
yield(Promise.all(coroutines), "completed")
print("all coroutines have resolved")
awaiting a single coroutine to finish
var coroutine = item.some_func()
# do more things ...
yield(Promise.resolve(coroutine), "completed")
the second example is more of a helper function to avoid having to check if the coroutine is_valid
before yielding.
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!
Most helpful comment
For awaiting the first signal and last signal in a set, I have a workaround like this (singleton
coroutines.gd
):Usage:
It's not pretty but it works.