#async-task #bevy-plugin #bevy #async #tasks #async-api #task

bevy_mod_async

Bevy游戏引擎的便捷异步任务插件

8个版本 (破坏性更新)

0.7.0 2024年7月5日
0.6.0 2024年6月10日
0.5.0 2024年5月3日
0.4.0 2024年5月3日
0.1.0 2024年1月26日

#353 in 游戏开发

每月32次下载

MIT/Apache

7MB
460

bevy_mod_async

bevy_mod_async是我为Bevy中的异步任务创建的一个更便捷的API。它建立在bevy_tasks的executor之上,因此不需要引入另一个异步运行时(尽管bevy_mod_async确实使用了tokio的一些类型来提供其异步接口——这在未来可以被替换)。

使用方法

添加 AsyncTasksPlugin 确保所有异步任务都将得到适当更新,并添加了 TaskContext 需要用于启动任务所需资源

use bevy_mod_async::prelude::*;
...
app.add_plugins(AsyncTasksPlugin);

之后,bevy_mod_async有两个主要的API:commands.spawn_task()(接受一个类型为 TaskContext 的单个参数的异步闭包)

commands.spawn_task(|cx| async move {
    ...
});

TaskContext::with_world,它用于专门、异步地访问世界

cx.with_world(|world| {
    // do anything with `world` here as in an exclusive system
    let e = world.spawn(()).id();
    world.despawn(e);
    world.resource_mut::<Counter>().0 += 1;
}).await;

在此API之上提供了一些便利方法

let e = cx.spawn(()).await;
cx.despawn(e).await;
let a = cx.load_asset::<Mesh>("model.glb#Mesh0").await.unwrap();

许多这些API返回 WithWorld,一个 Future,当等待它时,返回在 World 上执行命令的结果。这意味着在等待这个future之后,世界上的任何修改都将生效。由于WithWorld futures(默认情况下)每帧只推进一次,这也意味着每次 .await 通常会延迟一帧的执行。如果这不可取,任务也可以分离

cx.spawn(()).detach();

这将仍然将任务推送到Bevy的executor,但它不会挂起执行(这显然也意味着世界没有被修改)。

动机

普通的bevy_tasks有什么问题?嗯,Bevy启动异步任务的主要API使用AsyncTaskPool

let task = AsyncComputeTaskPool::get().spawn(my_task);

一旦您启动了一个新的任务,执行器就会负责轮询结果的完成。但您如何访问结果呢?您必须自己进行轮询,通常使用 Bevy 系统。

struct MyTask(Task<..>);
fn handle_task_completion(mut tasks: Query<&mut MyTask>, mut commands: Commands) {
    for mut task in &mut tasks {
        if task.is_finished() {
            let result = block_on(poll_once(&mut task.0));
            // handle result
        }
    }
}

如果您正在启动大量类似的异步任务,这还不错。对于一次性异步任务,例如响应资源加载,这就很多样板代码。相反,您可以在 async 块中处理异步任务的完成。

AsyncComputeTaskPool::get().spawn(async move {
    let result = my_task.await;
    println!("{result}");
}).detach();

当然,这种方法仅适用于您能够在不访问 ECS 的情况下舒适地处理结果,因为任务本身无法访问 World。如果您想要避免创建特定于任务的系统,另一种方法是用通道将任务发送到任务处理系统,并返回它们的结果。这是 bevy_mod_tasks 使用的方案。有一个专用的系统来运行队列中的任务,一个资源来累积它们,以及通道在专用系统与您的任务之间传递任务和结果。

这显然有一些限制:所有对 World 的访问都必须等到下一次这个专用系统运行时才能获取结果,并且这些访问都不能并行化。另一方面,如果您只是做些像加载资源这样简单的事情,等待它加载完成,然后再将其加载到场景中,bevy_mod_async 提供了一个简单的 API,以易于阅读、线性的方式编写这个逻辑。在这种情况下,性能影响可以忽略不计:任务只运行一次,并且是在加载时间,而不是在热游戏循环中。

示例

hello_world

演示了 TaskContext 的一些异步 API,并试图简单地解释其内部工作原理。展示了如何订阅异步事件流以及基本的 UI 响应。

async_asset

演示了异步加载资源。启动加载界面,然后在场景准备好加载时销毁它。

timers

演示了 sleep API,以及在没有界面运行的 Bevy 中启动异步任务。

Bevy 版本

bevy bevy_mod_async
0.12 0.1-0.2
0.13 0.3-0.6
0.14 0.7

贡献

欢迎 Pull Request。我必须承认我不是异步 Rust 编程的世界级专家,也没有对 Bevy 的 ECS 内部有深刻的理解。如果有人有性能、易用性或其他改进的想法,我愿意接受贡献。

依赖项

~12–48MB
~775K SLoC