#coroutine #godot #async #api-bindings #gdextension

nightly gdext_coroutines

Run Rust Async functions and Coroutines in Godot 4.2+ (through GDExtension), inspired on Unity's Coroutines design

4 releases

0.4.2 Aug 21, 2024
0.4.1 Aug 1, 2024
0.4.0 Jul 12, 2024
0.3.0 Jul 11, 2024
0.1.1 Jun 27, 2024

#830 in Game dev

Download history 285/week @ 2024-06-24 5/week @ 2024-07-01 354/week @ 2024-07-08 4/week @ 2024-07-15 21/week @ 2024-07-22 132/week @ 2024-07-29 4/week @ 2024-08-05 139/week @ 2024-08-19 5/week @ 2024-08-26

151 downloads per month

MIT license

33KB
597 lines

gdext_coroutines

"Run Rust coroutines and async code in Godot 4.2+ (through GDExtension), inspired on Unity's Coroutines design."

Beware

This crate uses 5 nightly(unstable) features:

#![feature(coroutines)]
#![feature(coroutine_trait)]
#![feature(stmt_expr_attributes)]
#![feature(unboxed_closures)]
#![cfg_attr(feature = "async", feature(async_fn_traits))]

It also requires GdExtension's experimental_threads feature

Setup

Add the dependency to your Cargo.toml file:

[dependencies]
gdext_coroutines = "0.4"

What does this do?

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), the implementation uses the crate smol and requires the feature async.

#![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.");
		});

	node.start_async_task(
		async {
			godot_print!("Executing async code!");
			smol::Timer::after(Duration::from_secs(10)).await;
			godot_print!("Async function finished after 10 real time seconds!");
		});
}

For more examples, check the integration_tests folder in the repository.


How does this do?

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): checks if the future has finished executing.
#[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_process to run).
  • If the parent is queued free, the coroutine is also queued free, and its finished signal never triggers.

Also 1

You can await coroutines from GdScript, using the signal finished:

var coroutine: SpireCoroutine = ..
var result = await coroutine.finished

result contains the return value of your coroutine/future.


Also 2

You can make your own custom types of yields, just implement the trait KeepWaiting:

pub trait KeepWaiting {
	/// The coroutine calls this to check if it should keep waiting
	fn keep_waiting(&mut self) -> bool;
}

Then you can use that trait like this:

let my_custom_yield: dyn KeepWaiting = ...;

yield Yield::Dyn(Box::new(my_custom_yield));

Dependencies

~5–14MB
~229K SLoC