bevy_mod_async 0.11.0

Ergonomic async tasks plugin for the Bevy game engine
Documentation
use std::{future::Future, time::Duration};

use bevy_app::{App, Update};
use bevy_ecs::{
    component::Component,
    entity::Entity,
    system::{Commands, Query, Res},
    world::World,
};
use bevy_time::Time;
use futures::TryFutureExt;
use tokio::sync::oneshot;

use crate::TaskContext;

pub fn time_plugin(app: &mut App) {
    app.add_systems(Update, (advance_timeout_after, advance_timeout_at));
}

pub trait TimingTaskExt {
    fn sleep(&self, duration: Duration) -> impl Future<Output = ()>;
    fn sleep_until(&self, duration: Duration) -> impl Future<Output = ()>;
}

impl TimingTaskExt for TaskContext {
    fn sleep(&self, duration: Duration) -> impl Future<Output = ()> {
        let (tx, rx) = oneshot::channel();
        self.with_world(move |world| {
            world.spawn(TimeoutAfter(duration, tx));
        })
        .detach();
        rx.unwrap_or_else(|_| ())
    }

    fn sleep_until(&self, elapsed_since_startup: Duration) -> impl Future<Output = ()> {
        let (tx, rx) = oneshot::channel();
        self.with_world(move |world| {
            world.spawn(TimeoutAt(elapsed_since_startup, tx));
        })
        .detach();
        rx.unwrap_or_else(|_| ())
    }
}

#[derive(Component)]
pub struct TimeoutAfter(Duration, oneshot::Sender<()>);

#[derive(Component)]
pub struct TimeoutAt(Duration, oneshot::Sender<()>);

pub fn advance_timeout_after(
    mut timeouts: Query<(Entity, &mut TimeoutAfter)>,
    time: Res<Time>,
    mut commands: Commands,
) {
    for (e, mut timeout) in &mut timeouts {
        if let Some(new_timeout) = timeout.0.checked_sub(time.delta()) {
            timeout.0 = new_timeout;
        } else {
            commands.queue(move |world: &mut World| {
                let Ok(mut e) = world.get_entity_mut(e) else {
                    return;
                };
                if let Some(timeout) = e.take::<TimeoutAfter>() {
                    timeout.1.send(()).ok();
                }
                e.despawn();
            });
        }
    }
}

pub fn advance_timeout_at(
    timeouts: Query<(Entity, &TimeoutAt)>,
    time: Res<Time>,
    mut commands: Commands,
) {
    for (e, timeout) in &timeouts {
        if time.elapsed() >= timeout.0 {
            commands.queue(move |world: &mut World| {
                let Ok(mut e) = world.get_entity_mut(e) else {
                    return;
                };
                if let Some(timeout) = e.take::<TimeoutAt>() {
                    timeout.1.send(()).ok();
                }
                e.despawn();
            });
        }
    }
}