bevy_coroutine_system 0.1.0

A coroutine system for Bevy game engine
Documentation

🚀 Bevy Coroutine System

Rust Bevy

English | 中文

一个为 Bevy 游戏引擎设计的协程系统,允许系统在多帧执行并支持暂停/恢复。

实现丑陋,但是有用的东西

✨ 特性

  • 🎮 多帧执行: 系统可以跨多个游戏帧执行
  • ⏸️ 暂停/恢复: 支持在任意点暂停执行并在后续帧恢复
  • 🔄 异步操作: 内置对异步操作的支持(如延时等待)
  • 🛠️ 简单易用: 通过宏自动处理复杂的生命周期和状态管理
  • 🔓 非独占访问: 不需要独占 World,只借用需要的系统参数
  • 🔃 实时数据更新: 每次 yield 恢复后,自动获取最新的组件数据
  • 🎯 非拷贝: 直接遍历原始组件数据,无需额外的数据拷贝

📦 安装

⚠️ 注意: 该库需要 Rust nightly 版本,因为使用了不稳定的协程特性。

1️⃣ 添加依赖

[dependencies]
bevy = "0.16"
bevy_coroutine_system = { path = "path/to/bevy_coroutine_system" }

2️⃣ 设置 nightly 工具链

rustup override set nightly

3️⃣ 启用必需的 feature flags

在你的 crate 根文件(main.rslib.rs)的顶部添加:

#![feature(coroutines, coroutine_trait)]

⚠️ 重要:这些 feature flags 是必需的,因为宏生成的代码会使用 yield 语法和协程相关类型。如果不添加,编译会失败并提示缺少这些特性。

🎯 基础用法

#![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>,
) {
    // 第一帧执行
    for mut transform in query.iter_mut() {
        transform.translation.x += 10.0;
    }
    
    // 暂停1秒(支持原生 yield 语法)
    yield sleep(Duration::from_secs(1));
    
    // 恢复后继续执行
    for mut transform in query.iter_mut() {
        transform.translation.y += 10.0;
    }
}

fn main() {
    let mut app = App::new();
    
    app.add_plugins((DefaultPlugins, CoroutinePlugin));
    
    // 注册协程系统
    app.register_coroutine(my_coroutine_system, my_coroutine_system::id());
    
    // 添加触发系统
    app.add_systems(Update, trigger_coroutine);
    
    app.run();
}

fn trigger_coroutine(
    mut commands: Commands,
    keyboard: Res<ButtonInput<KeyCode>>,
) {
    if keyboard.just_pressed(KeyCode::Space) {
        // 按空格键触发协程
        commands.run_system_cached(my_coroutine_system);
    }
}

协程系统的执行方式

协程系统可以通过两种方式执行,它们的行为有区别:

方式1:注册并手动触发(一次性执行)

注册协程系统后,通过手动触发来执行。协程会自动连续运行直到完成:

// 注册协程系统
app.register_coroutine(my_coroutine_system, my_coroutine_system::id());

// 手动触发(例如响应按键)
fn trigger_system(mut commands: Commands, keyboard: Res<ButtonInput<KeyCode>>) {
    if keyboard.just_pressed(KeyCode::Space) {
        commands.run_system_cached(my_coroutine_system);
    }
}

这种方式下,协程会执行一次完整的流程直到结束。

方式2:作为常规系统(循环执行)

将协程系统添加为常规的 Bevy 系统,无需使用 register_coroutine

// 直接添加为 Update 系统
app.add_systems(Update, my_coroutine_system);

这种方式下,协程会不断重复执行。例如:

#[coroutine_system]
fn repeating_coroutine() {
    info!("1");
    yield sleep(Duration::from_secs(1));
    info!("2");
}

输出将会是:1, 2, 1, 2, 1, 2...(每个循环间隔1秒)

内置异步函数

本库提供了四个内置的异步函数,用于控制协程的执行流程:

1. sleep(duration) - 延时等待

等待指定的时间后继续执行:

use std::time::{Duration, Instant};

// 等待1秒
let wake_time: Instant = yield sleep(Duration::from_secs(1));
// wake_time 是唤醒时的时间戳

2. next_frame() - 等待下一帧

暂停执行直到下一帧:

// 等待一帧
yield next_frame();
// 返回值是 (),通常不需要接收

3. noop() - 空操作

立即返回,不执行任何操作。主要用于解决条件分支中的借用检查问题。

当在条件分支中使用 yield 时,如果只有部分分支有 yield,可能会遇到 "borrow may still be in use when coroutine yields" 错误:

// ❌ 错误示例
if condition {
    yield sleep(Duration::from_secs(1));  // 只有一个分支有 yield
}
// 使用参数时报错

// ✅ 正确示例
if condition {
    yield sleep(Duration::from_secs(1));
}
yield noop(); // 确保所有控制流路径都有 yield 点

4. spawn_blocking_task(closure) - 执行阻塞任务

在后台线程中执行阻塞代码,避免阻塞游戏主线程。可以执行文件I/O、网络请求、长时间计算等操作:

let response: String = yield spawn_blocking_task(move || {
    // 这里可以安全地执行阻塞操作
});
  • 任务在单独的线程中执行,不会阻塞游戏主线程
  • 协程会在每帧检查线程是否完成
  • 任务完成后自动恢复执行后续操作

⚠️ 这里的返回值类型需要匹配手动确认匹配,编译不会报错,但运行时会panic!

获取异步操作的返回值

你可以通过明确指定类型来获取 yield 表达式的返回值:

// 明确指定返回类型
let result: std::time::Instant = yield sleep(Duration::from_secs(1));

⚠️ 警告:如果指定的类型与实际返回类型不匹配,程序会 panic!请确保使用正确的类型(见上述各函数说明)。

🔍 工作原理

📋 概述

  1. 🔮 过程宏转换: #[coroutine_system] 宏将协程函数转换为常规的、可多次重复执行的 Bevy 系统函数
  2. 💾 状态管理: 每个协程的状态由 CoroutineTask 结构管理
  3. 🔗 参数传递: 使用裸指针机制绕过 Bevy 的生命周期限制
  4. ⚡ 异步集成: Future 在每帧被轮询直到完成

🔬 宏展开示例

当你编写这样的协程系统:

#[coroutine_system]
fn my_coroutine_system(
    mut query: Query<&mut Transform>,
) {
    // 修改位置
    for mut transform in query.iter_mut() {
        transform.translation.x += 10.0;
    }
    
    // 暂停1秒
    yield sleep(Duration::from_secs(1));
    
    // 恢复后继续
    for mut transform in query.iter_mut() {
        transform.translation.y += 10.0;
    }
}

宏会将其展开为类似这样的伪代码:

// 自动生成的参数结构体
#[derive(SystemParam)]
struct MyCoroutineSystemParams<'w, 's> {
    query: Query<'w, 's, &mut Transform>,
}

// 实际的系统函数
fn my_coroutine_system<'w, 's>(
    params: MyCoroutineSystemParams<'w, 's>,
    mut task: Local<CoroutineTask<CoroutineTaskInput<MyCoroutineSystemParams<'static, 'static>>>>,
    mut running_task: ResMut<RunningCoroutines>,
) {
    // 首次运行时创建协程
    if task.coroutine.is_none() {
        task.coroutine = Some(Box::pin(
            #[coroutine]
            move |mut input: CoroutineTaskInput<MyCoroutineSystemParams<'static, 'static>>| {
                // 获取参数的裸指针
                let params = input.data_mut();
                let query = &mut params.query;
                
                // 原始函数体的第一部分
                for mut transform in query.iter_mut() {
                    transform.translation.x += 10.0;
                }
                
                // yield 表达式被转换为协程的 yield
                input = yield sleep(Duration::from_secs(1));
                
                // yield 后重新获取参数(重要!)
                let params = input.data_mut();
                let query = &mut params.query;
                
                // 原始函数体的剩余部分
                for mut transform in query.iter_mut() {
                    transform.translation.y += 10.0;
                }
            }
        ));
        
        // 标记系统为运行中
        running_task.systems.insert(my_coroutine_system::id(), ());
    }
    
    // 处理异步操作(如sleep)
    let mut async_result = None;
    if let Some(fut) = &mut task.fut {
        // 轮询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未完成,下帧继续
        }
    }
    
    // 创建协程输入,包含参数指针和异步结果
    let input = CoroutineTaskInput {
        data_ptr: Some(unsafe { NonNull::new_unchecked(&params as *const _ as *mut _) }),
        async_result,
    };
    
    // 恢复协程执行
    if let Some(coroutine) = &mut task.coroutine {
        match coroutine.as_mut().resume(input) {
            CoroutineState::Yielded(future) => {
                // 协程yield了一个Future,保存起来下帧继续
                task.fut = Some(future);
            }
            CoroutineState::Complete(()) => {
                // 协程执行完毕,清理状态
                task.coroutine = None;
                running_task.systems.remove(my_coroutine_system::id());
                return;
            }
        }
    }
}

// 生成的模块,提供唯一ID
pub mod my_coroutine_system {
    pub fn id() -> &'static str {
        concat!(module_path!(), "::my_coroutine_system")
    }
}

🔑 关键机制

  1. 🔐 生命周期处理: 使用裸指针(NonNull)传递参数,绕过 Rust 的生命周期检查
  2. 📦 协程状态: 通过 Local<CoroutineTask> 保存协程状态,实现跨帧持久化
  3. ⚡ 异步支持: yield 的 Future 在每帧被轮询,直到完成
  4. 🔄 自动注册: RunningCoroutines 资源跟踪所有活跃的协程,确保它们每帧执行

📚 示例

查看 examples 目录获取更多示例:

  • 📝 simple.rs - 简单的协程系统示例
  • 🌱 minimal.rs - 最小化的协程系统
  • 🌐 http_example.rs - HTTP请求示例,演示如何使用 spawn_blocking_task 执行异步HTTP请求

运行示例:

cargo run --example simple
cargo run --example minimal
cargo run --example http_example

⚠️ 限制

  • 🔧 需要 Rust nightly 版本
  • 🚧 协程特性仍处于实验阶段
  • 💡 使用不安全的裸指针传递参数
  • 📊 宏覆盖范围有限,有些参数可能没有及时支持

🤝 贡献

欢迎贡献!请随时提交 Issue 或 Pull Request。

📄 License

MIT OR Apache-2.0