"Run Rust coroutines and async code in Godot 4.4+ (through GDExtension), inspired on Unity's Coroutines design."
This crate uses 4 nightly(unstable) features:
#![feature(coroutines)]
#![feature(coroutine_trait)]
#![feature(stmt_expr_attributes)]
#![feature(unboxed_closures)]It also requires GdExtension's experimental_threads feature
Add the dependency to your Cargo.toml file:
[dependencies]
gdext_coroutines = "1.0.0"Allows you to execute code in an asynchronous manner, the coroutines of this crate work very much like Unity's.
It also allows you to execute async code(futures) via gdext-rust's task system (godot::task::spawn).
#![feature(coroutines)]
use gdext_coroutines::prelude::*;
use godot::prelude::*;
fn run_some_routines(node: Gd<Label>) {
node.start_coroutine(
#[coroutine] || {
godot_print!("Starting coroutine");
godot_print!("Waiting for 5 seconds...");
yield seconds(5.0);
godot_print!("5 seconds have passed!");
godot_print!("Waiting for 30 frames");
yield frames(30);
godot_print!("30 frames have passed!");
godot_print!("Waiting until pigs start flying...");
let pig: Gd<Node2D> = create_pig();
yield wait_until(move || pig.is_flying());
godot_print!("Wow! Pigs are now able to fly! Somehow...");
godot_print!("Waiting while pigs are still flying...");
let pig: Gd<Node2D> = grab_pig();
yield wait_while(move || pig.is_flying());
godot_print!("Finally, no more flying pigs, oof.");
godot_print!("Waiting for a signal...");
let signal = Signal::from_object_signal(&node, "some_signal");
yield wait_for_signal_untyped(signal);
godot_print!("Signal received!");
});
node.start_async_task(
async {
godot_print!("Executing async code!");
some_async_fn().await;
godot_print!("Async function finished!");
});
}For more examples, check the integration_tests folder in the repository.
A Coroutine is a struct that derives Node
#[derive(GodotClass)]
#[class(no_init, base = Node)]
pub struct SpireCoroutine { /* .. */ }When you invoke start_coroutine(), start_async_task(), or spawn(), a SpireCoroutine node is created, then added as a child of the caller.
Then, on every frame:
- Rust Coroutines(
start_coroutine): polls the current yield to advance its inner function. - Rust Futures(
start_async_task): polls until the spawned gdext-rust task finishes.
#[godot_api]
impl INode for SpireCoroutine {
fn process(&mut self, delta: f64) {
if !self.paused && self.poll_mode == PollMode::Process {
self.run(delta);
}
}
fn physics_process(&mut self, delta: f64) {
if !self.paused && self.poll_mode == PollMode::Physics {
self.run(delta);
}
}
}Then it automatically destroys itself after finishing:
fn run(&mut self, delta_time: f64) {
if let Some(result) = self.poll(delta_time) {
self.finish_with(result);
}
}
pub fn finish_with(&mut self, result: Variant) {
/* .. */
self.base_mut().emit_signal(SIGNAL_FINISHED.into(), &[result]);
self.de_spawn();
}Since the coroutine is a child node of whoever created it, the behavior is tied to its parent:
- If the parent exits the scene tree, the coroutine pauses running (since it requires
_process/_physics_processto run). - If the parent is queued free, the coroutine is also queued free, and its
finishedsignal never triggers.
yield frames(5);yield seconds(2.5);yield wait_until(move || some_condition());yield wait_while(move || still_busy());let sig = node.signals().some_signal();
yield wait_for_signal(&sig);For untyped signals:
let sig = Signal::from_object_signal(&node, "some_signal");
yield wait_for_signal_untyped(sig);let other = node.start_coroutine(#[coroutine] || { yield frames(10); });
yield other.wait_until_finished();var coroutine: SpireCoroutine = ..
var result = await coroutine.finishedresult contains the return value of your coroutine/future.
pub trait KeepWaiting {
/// The coroutine calls this to check if it should keep waiting
fn keep_waiting(&mut self, delta_time: f64) -> bool;
}Then you can use that trait like this:
let my_custom_yield: dyn KeepWaiting = ...;
yield Yield::Dyn(Box::new(my_custom_yield));Otherwise, this crate's godot classes will not be registered in Godot.
This is a known issue in gdext-rust, it's not related to gdext-coroutines.