Created
February 15, 2024 14:42
-
-
Save noidexe/6680e404a6e38c5d3d7e35d6f95a9591 to your computer and use it in GitHub Desktop.
Godot 3-style coroutines in Godot 4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends Node3D | |
################################################################## | |
## your scientists were so preoccupied with whether or not they ## | |
## could that they didn't stop to think if they should. ## | |
################################################################## | |
class YeOldeGDScriptFunctionState extends RefCounted: | |
#region Private | |
var _suspended := false # state of the coroutine | |
var _resume_arg : Variant # argument passed to resume() | |
var _ret : Variant # return value of the coroutine | |
signal _resumed # internal signal to handle resuming | |
func _init(f: Callable): | |
_ret = await f.call(self) # await the actual coroutine to return | |
is_valid = false # function already returned, coroutine no longer valid | |
completed.emit(_ret) # emit the return value in a completed signal | |
#endregion | |
#region Public | |
signal completed(_ret) # used to await until the coroutine returns | |
var is_valid := true # to check if the coroutine is still running | |
var last_yield : Variant # a value can be store here on every yield | |
# Suspends execution. Like old yield, but you can yield a value using p_yield | |
func yield_is_still_a_reserved_word_sorry( p_yield = null ): | |
_suspended = true # set state | |
last_yield = p_yield # set yield value | |
await _resumed # wait to be resumed by the _resume signal | |
_suspended = false # set state | |
return _resume_arg # return the argument passet to resume() | |
# Resumes execution. You can pass an argument as a return value to the last yield call | |
func resume( arg = null): | |
assert(is_valid, "Well.. you broke it. I hope you're happy.") | |
_resume_arg = arg # store argument | |
_resumed.emit() # emit _resumed signal, this will immediately resume the coroutine | |
# if we reach here the coroutine has already halted again, or returned | |
if is_valid: | |
# this means the coroutine is still running, return a YeOlde...etc | |
return self | |
else: | |
# this means the coroutine has finished, return its return value | |
return _ret | |
#endregion | |
# To make it work our 3.x-style coroutines need to take a YeOldeGDScriptFunctionState as first arugment | |
func print_numbers(c : YeOldeGDScriptFunctionState, from : int, to : int): | |
for i in range(from, to+1): | |
await c.yield_is_still_a_reserved_word_sorry(i) | |
return "I counted %s numbers. Yay!" % [to-from+1] | |
func count_to_x(c: YeOldeGDScriptFunctionState, to: int): | |
for i in to: | |
print("%s!" % [i+1]) | |
await get_tree().create_timer(1.0).timeout | |
return to | |
func my_func(c : YeOldeGDScriptFunctionState): | |
print("Hello") | |
print(await c.yield_is_still_a_reserved_word_sorry()) | |
return "cheers!" | |
func _ready(): | |
# call `print_numbers(2,6)` as a YeOldeGDScriptFunctionState | |
var counter = await YeOldeGDScriptFunctionState.new(print_numbers.bind(2,6)) | |
while(counter is YeOldeGDScriptFunctionState and counter.is_valid): | |
print(counter.last_yield) | |
counter = counter.resume() | |
print(counter) | |
var another_counter = await YeOldeGDScriptFunctionState.new(count_to_x.bind(5)) | |
# This time we wait till it's done | |
print("Total numbers: ", await another_counter.completed) | |
# Here we launch three counters in parallel and wait for all of them | |
var a = await YeOldeGDScriptFunctionState.new(count_to_x.bind(1)) | |
var b = await YeOldeGDScriptFunctionState.new(count_to_x.bind(10)) | |
var c = await YeOldeGDScriptFunctionState.new(count_to_x.bind(3)) | |
# Strictly we should also check if they are YeOlde...etc | |
# The reason to check if they are valid is that for example | |
# by the time we try to await for c it has already emited the signal | |
if a.is_valid: | |
await a.completed | |
print("A completed") | |
if b.is_valid: | |
await b.completed | |
print("B completed") | |
if c.is_valid: | |
await c.completed | |
print("C completed") | |
print("All done!") | |
# Godot 3.5 example from https://docs.godotengine.org/en/3.5/tutorials/scripting/gdscript/gdscript_basics.html#coroutines-with-yield | |
var y = await YeOldeGDScriptFunctionState.new(my_func) | |
# Function state saved in 'y'. | |
print(y.resume("world")) | |
print(y.is_valid) | |
# 'y' resumed and is now an invalid state. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment