| Crates.io | bevy_coroutine_system_macro |
| lib.rs | bevy_coroutine_system_macro |
| version | 0.1.1 |
| created_at | 2025-08-23 13:52:41.894918+00 |
| updated_at | 2025-08-23 13:59:27.454309+00 |
| description | Procedural macros for bevy_coroutine_system |
| homepage | |
| repository | https://github.com/molixianggu/bevy_coroutine_system |
| max_upload_size | |
| id | 1807531 |
| size | 42,448 |
English | 中文
A coroutine system designed for the Bevy game engine, allowing systems to execute across multiple frames with pause/resume support.
Ugly implementation, but useful stuff
⚠️ Note: This library requires Rust nightly version due to the use of unstable coroutine features.
[dependencies]
bevy = "0.16"
bevy_coroutine_system = { path = "path/to/bevy_coroutine_system" }
rustup override set nightly
Add the following at the top of your crate root file (main.rs or lib.rs):
#![feature(coroutines, coroutine_trait)]
⚠️ Important: These feature flags are required because the macro-generated code uses yield syntax and coroutine-related types. Without them, compilation will fail with missing feature errors.
#![feature(coroutines, coroutine_trait)]
use bevy::prelude::*;
use bevy_coroutine_system::prelude::*;
use std::time::Duration;
#[coroutine_system]
fn my_coroutine_system(
mut commands: Commands,
mut query: Query<&mut Transform>,
) {
// Execute on first frame
for mut transform in query.iter_mut() {
transform.translation.x += 10.0;
}
// Pause for 1 second (supports native yield syntax)
yield sleep(Duration::from_secs(1));
// Continue execution after resume
for mut transform in query.iter_mut() {
transform.translation.y += 10.0;
}
}
fn main() {
let mut app = App::new();
app.add_plugins((DefaultPlugins, CoroutinePlugin));
// Register the coroutine system
app.register_coroutine(my_coroutine_system, my_coroutine_system::id());
// Add trigger system
app.add_systems(Update, trigger_coroutine);
app.run();
}
fn trigger_coroutine(
mut commands: Commands,
keyboard: Res<ButtonInput<KeyCode>>,
) {
if keyboard.just_pressed(KeyCode::Space) {
// Trigger coroutine on spacebar press
commands.run_system_cached(my_coroutine_system);
}
}
Coroutine systems can be executed in two ways, with behavioral differences:
After registering a coroutine system, execute it through manual triggering. The coroutine will run continuously until completion:
// Register the coroutine system
app.register_coroutine(my_coroutine_system, my_coroutine_system::id());
// Manual trigger (e.g., responding to keyboard input)
fn trigger_system(mut commands: Commands, keyboard: Res<ButtonInput<KeyCode>>) {
if keyboard.just_pressed(KeyCode::Space) {
commands.run_system_cached(my_coroutine_system);
}
}
In this mode, the coroutine executes once through its complete flow until it finishes.
Add the coroutine system as a regular Bevy system, without using register_coroutine:
// Add directly as an Update system
app.add_systems(Update, my_coroutine_system);
In this mode, the coroutine will execute repeatedly. For example:
#[coroutine_system]
fn repeating_coroutine() {
info!("1");
yield sleep(Duration::from_secs(1));
info!("2");
}
The output will be: 1, 2, 1, 2, 1, 2... (with a 1-second interval between each loop)
This library provides four built-in async functions to control coroutine execution flow:
sleep(duration) - Timed DelayWait for a specified duration before continuing:
use std::time::{Duration, Instant};
// Wait for 1 second
let wake_time: Instant = yield sleep(Duration::from_secs(1));
// wake_time is the timestamp when awakened
next_frame() - Wait for Next FramePause execution until the next frame:
// Wait for one frame
yield next_frame();
// Returns (), usually no need to capture the result
noop() - No OperationReturns immediately without doing anything. Mainly used to solve borrow checker issues in conditional branches.
When using yield in conditional branches where only some branches have yield, you may encounter "borrow may still be in use when coroutine yields" error:
// ❌ Incorrect example
if condition {
yield sleep(Duration::from_secs(1)); // Only one branch has yield
}
// Error when using parameters
// ✅ Correct example
if condition {
yield sleep(Duration::from_secs(1));
}
yield noop(); // Ensures all control flow paths have a yield point
spawn_blocking_task(closure) - Execute Blocking TaskExecute blocking code in a background thread to avoid blocking the main game thread. Can be used for file I/O, network requests, long computations, etc.:
let response: String = yield spawn_blocking_task(move || {
// It's safe to execute blocking operations here
});
⚠️ The return type here needs to be manually confirmed to match. It won't cause a compilation error, but will panic at runtime if incorrect!
You can get return values from yield expressions by explicitly specifying the type:
// Explicitly specify return type
let result: std::time::Instant = yield sleep(Duration::from_secs(1));
⚠️ Warning: If the specified type doesn't match the actual return type, the program will panic! Make sure to use the correct types (see the function descriptions above).
#[coroutine_system] macro transforms coroutine functions into regular, repeatable Bevy system functionsCoroutineTask structureWhen you write a coroutine system like this:
#[coroutine_system]
fn my_coroutine_system(
mut query: Query<&mut Transform>,
) {
// Modify position
for mut transform in query.iter_mut() {
transform.translation.x += 10.0;
}
// Pause for 1 second
yield sleep(Duration::from_secs(1));
// Continue after resume
for mut transform in query.iter_mut() {
transform.translation.y += 10.0;
}
}
The macro expands it to something like this pseudocode:
// Auto-generated parameter struct
#[derive(SystemParam)]
struct MyCoroutineSystemParams<'w, 's> {
query: Query<'w, 's, &mut Transform>,
}
// Actual system function
fn my_coroutine_system<'w, 's>(
params: MyCoroutineSystemParams<'w, 's>,
mut task: Local<CoroutineTask<CoroutineTaskInput<MyCoroutineSystemParams<'static, 'static>>>>,
mut running_task: ResMut<RunningCoroutines>,
) {
// Create coroutine on first run
if task.coroutine.is_none() {
task.coroutine = Some(Box::pin(
#[coroutine]
move |mut input: CoroutineTaskInput<MyCoroutineSystemParams<'static, 'static>>| {
// Get raw pointer to parameters
let params = input.data_mut();
let query = &mut params.query;
// First part of original function body
for mut transform in query.iter_mut() {
transform.translation.x += 10.0;
}
// yield expression is converted to coroutine yield
input = yield sleep(Duration::from_secs(1));
// Re-fetch parameters after yield (important!)
let params = input.data_mut();
let query = &mut params.query;
// Remaining part of original function body
for mut transform in query.iter_mut() {
transform.translation.y += 10.0;
}
}
));
// Mark system as running
running_task.systems.insert(my_coroutine_system::id(), ());
}
// Handle async operations (like sleep)
let mut async_result = None;
if let Some(fut) = &mut task.fut {
// Poll the Future
match fut.as_mut().poll(&mut Context::from_waker(&Waker::noop())) {
Poll::Ready(v) => {
async_result = Some(v);
task.fut = None;
}
Poll::Pending => return, // Future not ready, continue next frame
}
}
// Create coroutine input with parameter pointer and async result
let input = CoroutineTaskInput {
data_ptr: Some(unsafe { NonNull::new_unchecked(¶ms as *const _ as *mut _) }),
async_result,
};
// Resume coroutine execution
if let Some(coroutine) = &mut task.coroutine {
match coroutine.as_mut().resume(input) {
CoroutineState::Yielded(future) => {
// Coroutine yielded a Future, save it for next frame
task.fut = Some(future);
}
CoroutineState::Complete(()) => {
// Coroutine completed, clean up state
task.coroutine = None;
running_task.systems.remove(my_coroutine_system::id());
return;
}
}
}
}
// Generated module providing unique ID
pub mod my_coroutine_system {
pub fn id() -> &'static str {
concat!(module_path!(), "::my_coroutine_system")
}
}
NonNull) to pass parameters, bypassing Rust's lifetime checksLocal<CoroutineTask> for cross-frame persistenceRunningCoroutines resource tracks all active coroutines, ensuring they execute each frameCheck the examples directory for more examples:
simple.rs - Simple coroutine system exampleminimal.rs - Minimal coroutine systemhttp_example.rs - HTTP request example, demonstrates how to use spawn_blocking_task to execute async HTTP requestsRun examples:
cargo run --example simple
cargo run --example minimal
cargo run --example http_example
Contributions are welcome! Feel free to submit Issues or Pull Requests.
MIT OR Apache-2.0