🚀 Bevy Coroutine System
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
✨ Features
- 🎮 Multi-frame Execution: Systems can execute across multiple game frames
- ⏸️ Pause/Resume: Support for pausing execution at any point and resuming in subsequent frames
- 🔄 Async Operations: Built-in support for asynchronous operations (e.g., timed delays)
- 🛠️ Easy to Use: Automatically handles complex lifecycle and state management through macros
- 🔓 Non-exclusive Access: No need for exclusive World access, only borrows required system parameters
- 🔃 Real-time Data Updates: Automatically fetches the latest component data after each yield resume
- 🎯 No-copy: Directly iterates over raw component data without additional copying
📦 Installation
⚠️ Note: This library requires Rust nightly version due to the use of unstable coroutine features.
1️⃣ Add Dependencies
[]
= "0.16"
= { = "path/to/bevy_coroutine_system" }
2️⃣ Set up Nightly Toolchain
3️⃣ Enable Required Feature Flags
Add the following at the top of your crate root file (main.rs
or lib.rs
):
⚠️ 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.
🎯 Basic Usage
use *;
use *;
use Duration;
Execution Methods for Coroutine Systems
Coroutine systems can be executed in two ways, with behavioral differences:
Method 1: Register and Trigger Manually (One-time Execution)
After registering a coroutine system, execute it through manual triggering. The coroutine will run continuously until completion:
// Register the coroutine system
app.register_coroutine;
// Manual trigger (e.g., responding to keyboard input)
In this mode, the coroutine executes once through its complete flow until it finishes.
Method 2: As a Regular System (Loop Execution)
Add the coroutine system as a regular Bevy system, without using register_coroutine
:
// Add directly as an Update system
app.add_systems;
In this mode, the coroutine will execute repeatedly. For example:
The output will be: 1, 2, 1, 2, 1, 2...
(with a 1-second interval between each loop)
Built-in Async Functions
This library provides four built-in async functions to control coroutine execution flow:
1. sleep(duration)
- Timed Delay
Wait for a specified duration before continuing:
use ;
// Wait for 1 second
let wake_time: Instant = yield sleep;
// wake_time is the timestamp when awakened
2. next_frame()
- Wait for Next Frame
Pause execution until the next frame:
// Wait for one frame
yield next_frame;
// Returns (), usually no need to capture the result
3. noop()
- No Operation
Returns 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
// Error when using parameters
// ✅ Correct example
if condition
yield noop; // Ensures all control flow paths have a yield point
4. spawn_blocking_task(closure)
- Execute Blocking Task
Execute 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;
- The task runs in a separate thread, won't block the main game thread
- The coroutine checks each frame if the thread has completed
- Automatically resumes execution after the task completes
⚠️ 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!
Getting Return Values from Async Operations
You can get return values from yield expressions by explicitly specifying the type:
// Explicitly specify return type
let result: Instant = yield sleep;
⚠️ 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).
🔍 How It Works
📋 Overview
- 🔮 Procedural Macro Transformation: The
#[coroutine_system]
macro transforms coroutine functions into regular, repeatable Bevy system functions - 💾 State Management: Each coroutine's state is managed by the
CoroutineTask
structure - 🔗 Parameter Passing: Uses raw pointer mechanism to bypass Bevy's lifetime restrictions
- ⚡ Async Integration: Futures are polled each frame until completion
🔬 Macro Expansion Example
When you write a coroutine system like this:
The macro expands it to something like this pseudocode:
// Auto-generated parameter struct
// Actual system function
// Generated module providing unique ID
🔑 Key Mechanisms
- 🔐 Lifetime Handling: Uses raw pointers (
NonNull
) to pass parameters, bypassing Rust's lifetime checks - 📦 Coroutine State: Saves coroutine state via
Local<CoroutineTask>
for cross-frame persistence - ⚡ Async Support: Yielded Futures are polled each frame until completion
- 🔄 Auto Registration:
RunningCoroutines
resource tracks all active coroutines, ensuring they execute each frame
📚 Examples
Check the examples
directory for more examples:
- 📝
simple.rs
- Simple coroutine system example - 🌱
minimal.rs
- Minimal coroutine system - 🌐
http_example.rs
- HTTP request example, demonstrates how to usespawn_blocking_task
to execute async HTTP requests
Run examples:
⚠️ Limitations
- 🔧 Requires Rust nightly version
- 🚧 Coroutine features are still experimental
- 💡 Uses unsafe raw pointers for parameter passing
- 📊 Limited macro coverage, some parameters might not be supported yet
🤝 Contributing
Contributions are welcome! Feel free to submit Issues or Pull Requests.
📄 License
MIT OR Apache-2.0