Skip to content

Instantly share code, notes, and snippets.

@noidexe
Created February 15, 2024 14:42
Show Gist options
  • Save noidexe/6680e404a6e38c5d3d7e35d6f95a9591 to your computer and use it in GitHub Desktop.
Save noidexe/6680e404a6e38c5d3d7e35d6f95a9591 to your computer and use it in GitHub Desktop.
Godot 3-style coroutines in Godot 4
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