Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bevy example

Bevy has it’s own dedicated Moirai plugin, that handles all required integration.

use bevy::{camera::ScalingMode, prelude::*};
use futures::join;
use moirai::{coroutine::spawn, job::JobLocation};
use moirai_bevy::{
    Coroutines, MoiraiPlugin,
    coroutine::{next_frame, wait_secs, world},
};

fn main() {
    let mut app = App::new();
    app.add_plugins((DefaultPlugins, MoiraiPlugin))
        .add_systems(Startup, setup);
    app.run();
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    coroutines: Res<Coroutines>,
) {
    let font = asset_server.load("noto.ttf");

    commands.spawn((
        Camera2d,
        Projection::Orthographic(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 500.0,
            },
            ..OrthographicProjection::default_2d()
        }),
    ));

    let player_dialogue = commands
        .spawn((
            Text2d::default(),
            TextFont {
                font: font.clone(),
                font_size: 20.0,
                ..Default::default()
            },
            TextLayout::new_with_justify(Justify::Center).with_no_wrap(),
            TextColor::WHITE,
            Transform::from_xyz(0.0, 50.0, 0.0),
        ))
        .id();

    let player = commands
        .spawn((
            Mesh2d(meshes.add(Circle::new(15.0))),
            MeshMaterial2d(materials.add(Color::linear_rgb(0.0, 0.47, 0.95))),
            Transform::from_xyz(-150.0, 0.0, 0.0),
        ))
        .add_child(player_dialogue)
        .id();

    let enemy_dialogue = commands
        .spawn((
            Text2d::default(),
            TextFont {
                font: font.clone(),
                font_size: 20.0,
                ..Default::default()
            },
            TextLayout::new_with_justify(Justify::Center).with_no_wrap(),
            TextColor::WHITE,
            Transform::from_xyz(0.0, 50.0, 0.0),
        ))
        .id();

    let enemy = commands
        .spawn((
            Mesh2d(meshes.add(Rectangle::new(30.0, 30.0))),
            MeshMaterial2d(materials.add(Color::linear_rgb(0.95, 0.16, 0.22))),
            Transform::from_xyz(100.0, 0.0, 0.0),
        ))
        .add_child(enemy_dialogue)
        .id();

    coroutines.jobs.spawn(
        JobLocation::Local,
        cutscene(player, player_dialogue, enemy, enemy_dialogue),
    );
}

async fn cutscene(player: Entity, player_dialogue: Entity, enemy: Entity, enemy_dialogue: Entity) {
    move_character(player, vec2(-100.0, 0.0), 50.0).await;

    timed_dialogue(enemy_dialogue, "You dare challenge me, mortal?", 2.0).await;

    move_character(player, vec2(-15.0, 0.0), 100.0).await;

    timed_dialogue(player_dialogue, "I will defeat you!", 2.0).await;

    move_character(enemy, vec2(15.0, 0.0), 80.0).await;

    timed_dialogue(enemy_dialogue, "Prepare to meet your maker!", 2.0).await;

    join!(
        spawn(
            JobLocation::Local,
            move_character(player, vec2(-50.0, 0.0), 150.0),
        ),
        spawn(
            JobLocation::Local,
            move_character(enemy, vec2(50.0, 0.0), 150.0),
        )
    );

    join!(
        move_character(player, vec2(0.0, 0.0), 200.0),
        move_character(enemy, vec2(0.0, 0.0), 200.0)
    );

    join!(
        move_character(player, vec2(-150.0, 0.0), 200.0),
        move_character(enemy, vec2(-15.0, 0.0), 50.0),
    );

    timed_dialogue(player_dialogue, "Argh! Such pain!", 2.0).await;

    timed_dialogue(enemy_dialogue, "Victory is mine!", 2.0).await;

    timed_dialogue(player_dialogue, "You've won!", 1.0).await;
    timed_dialogue(player_dialogue, "I surrender...!", 1.0).await;
}

async fn show_dialogue(dialogue: Entity, content: impl ToString) {
    let world = world().await;
    let world = &mut *world.write().unwrap();
    let mut text = world.get_mut::<Text2d>(dialogue).unwrap();
    text.0 = content.to_string();
}

async fn hide_dialogue(dialogue: Entity) {
    let world = world().await;
    let world = &mut *world.write().unwrap();
    let mut text = world.get_mut::<Text2d>(dialogue).unwrap();
    text.0.clear();
}

async fn timed_dialogue(dialogue: Entity, text: impl ToString, duration: f32) {
    show_dialogue(dialogue, text).await;
    wait_secs(duration).await;
    hide_dialogue(dialogue).await;
}

async fn move_character(character: Entity, target: Vec2, speed: f32) {
    loop {
        {
            let world = world().await;
            let world = &mut *world.write().unwrap();
            let dt = world.resource::<Time>().delta_secs();

            let mut transform = world.get_mut::<Transform>(character).unwrap();
            let direction = (target - transform.translation.truncate()).normalize_or_zero();
            let distance = transform.translation.truncate().distance(target);

            let speed = speed * dt;
            if distance < speed {
                transform.translation = target.extend(transform.translation.z);
                break;
            } else {
                transform.translation += direction.extend(0.0) * speed;
            }
        }

        next_frame().await;
    }
}

So we can easily just grab Coroutines resource and spawn jobs.

use bevy::{camera::ScalingMode, prelude::*};
use futures::join;
use moirai::{coroutine::spawn, job::JobLocation};
use moirai_bevy::{
    Coroutines, MoiraiPlugin,
    coroutine::{next_frame, wait_secs, world},
};

fn main() {
    let mut app = App::new();
    app.add_plugins((DefaultPlugins, MoiraiPlugin))
        .add_systems(Startup, setup);
    app.run();
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    coroutines: Res<Coroutines>,
) {
    let font = asset_server.load("noto.ttf");

    commands.spawn((
        Camera2d,
        Projection::Orthographic(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 500.0,
            },
            ..OrthographicProjection::default_2d()
        }),
    ));

    let player_dialogue = commands
        .spawn((
            Text2d::default(),
            TextFont {
                font: font.clone(),
                font_size: 20.0,
                ..Default::default()
            },
            TextLayout::new_with_justify(Justify::Center).with_no_wrap(),
            TextColor::WHITE,
            Transform::from_xyz(0.0, 50.0, 0.0),
        ))
        .id();

    let player = commands
        .spawn((
            Mesh2d(meshes.add(Circle::new(15.0))),
            MeshMaterial2d(materials.add(Color::linear_rgb(0.0, 0.47, 0.95))),
            Transform::from_xyz(-150.0, 0.0, 0.0),
        ))
        .add_child(player_dialogue)
        .id();

    let enemy_dialogue = commands
        .spawn((
            Text2d::default(),
            TextFont {
                font: font.clone(),
                font_size: 20.0,
                ..Default::default()
            },
            TextLayout::new_with_justify(Justify::Center).with_no_wrap(),
            TextColor::WHITE,
            Transform::from_xyz(0.0, 50.0, 0.0),
        ))
        .id();

    let enemy = commands
        .spawn((
            Mesh2d(meshes.add(Rectangle::new(30.0, 30.0))),
            MeshMaterial2d(materials.add(Color::linear_rgb(0.95, 0.16, 0.22))),
            Transform::from_xyz(100.0, 0.0, 0.0),
        ))
        .add_child(enemy_dialogue)
        .id();

    coroutines.jobs.spawn(
        JobLocation::Local,
        cutscene(player, player_dialogue, enemy, enemy_dialogue),
    );
}

async fn cutscene(player: Entity, player_dialogue: Entity, enemy: Entity, enemy_dialogue: Entity) {
    move_character(player, vec2(-100.0, 0.0), 50.0).await;

    timed_dialogue(enemy_dialogue, "You dare challenge me, mortal?", 2.0).await;

    move_character(player, vec2(-15.0, 0.0), 100.0).await;

    timed_dialogue(player_dialogue, "I will defeat you!", 2.0).await;

    move_character(enemy, vec2(15.0, 0.0), 80.0).await;

    timed_dialogue(enemy_dialogue, "Prepare to meet your maker!", 2.0).await;

    join!(
        spawn(
            JobLocation::Local,
            move_character(player, vec2(-50.0, 0.0), 150.0),
        ),
        spawn(
            JobLocation::Local,
            move_character(enemy, vec2(50.0, 0.0), 150.0),
        )
    );

    join!(
        move_character(player, vec2(0.0, 0.0), 200.0),
        move_character(enemy, vec2(0.0, 0.0), 200.0)
    );

    join!(
        move_character(player, vec2(-150.0, 0.0), 200.0),
        move_character(enemy, vec2(-15.0, 0.0), 50.0),
    );

    timed_dialogue(player_dialogue, "Argh! Such pain!", 2.0).await;

    timed_dialogue(enemy_dialogue, "Victory is mine!", 2.0).await;

    timed_dialogue(player_dialogue, "You've won!", 1.0).await;
    timed_dialogue(player_dialogue, "I surrender...!", 1.0).await;
}

async fn show_dialogue(dialogue: Entity, content: impl ToString) {
    let world = world().await;
    let world = &mut *world.write().unwrap();
    let mut text = world.get_mut::<Text2d>(dialogue).unwrap();
    text.0 = content.to_string();
}

async fn hide_dialogue(dialogue: Entity) {
    let world = world().await;
    let world = &mut *world.write().unwrap();
    let mut text = world.get_mut::<Text2d>(dialogue).unwrap();
    text.0.clear();
}

async fn timed_dialogue(dialogue: Entity, text: impl ToString, duration: f32) {
    show_dialogue(dialogue, text).await;
    wait_secs(duration).await;
    hide_dialogue(dialogue).await;
}

async fn move_character(character: Entity, target: Vec2, speed: f32) {
    loop {
        {
            let world = world().await;
            let world = &mut *world.write().unwrap();
            let dt = world.resource::<Time>().delta_secs();

            let mut transform = world.get_mut::<Transform>(character).unwrap();
            let direction = (target - transform.translation.truncate()).normalize_or_zero();
            let distance = transform.translation.truncate().distance(target);

            let speed = speed * dt;
            if distance < speed {
                transform.translation = target.extend(transform.translation.z);
                break;
            } else {
                transform.translation += direction.extend(0.0) * speed;
            }
        }

        next_frame().await;
    }
}

For world manipulation, we can access entire ECS world.

use bevy::{camera::ScalingMode, prelude::*};
use futures::join;
use moirai::{coroutine::spawn, job::JobLocation};
use moirai_bevy::{
    Coroutines, MoiraiPlugin,
    coroutine::{next_frame, wait_secs, world},
};

fn main() {
    let mut app = App::new();
    app.add_plugins((DefaultPlugins, MoiraiPlugin))
        .add_systems(Startup, setup);
    app.run();
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    coroutines: Res<Coroutines>,
) {
    let font = asset_server.load("noto.ttf");

    commands.spawn((
        Camera2d,
        Projection::Orthographic(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 500.0,
            },
            ..OrthographicProjection::default_2d()
        }),
    ));

    let player_dialogue = commands
        .spawn((
            Text2d::default(),
            TextFont {
                font: font.clone(),
                font_size: 20.0,
                ..Default::default()
            },
            TextLayout::new_with_justify(Justify::Center).with_no_wrap(),
            TextColor::WHITE,
            Transform::from_xyz(0.0, 50.0, 0.0),
        ))
        .id();

    let player = commands
        .spawn((
            Mesh2d(meshes.add(Circle::new(15.0))),
            MeshMaterial2d(materials.add(Color::linear_rgb(0.0, 0.47, 0.95))),
            Transform::from_xyz(-150.0, 0.0, 0.0),
        ))
        .add_child(player_dialogue)
        .id();

    let enemy_dialogue = commands
        .spawn((
            Text2d::default(),
            TextFont {
                font: font.clone(),
                font_size: 20.0,
                ..Default::default()
            },
            TextLayout::new_with_justify(Justify::Center).with_no_wrap(),
            TextColor::WHITE,
            Transform::from_xyz(0.0, 50.0, 0.0),
        ))
        .id();

    let enemy = commands
        .spawn((
            Mesh2d(meshes.add(Rectangle::new(30.0, 30.0))),
            MeshMaterial2d(materials.add(Color::linear_rgb(0.95, 0.16, 0.22))),
            Transform::from_xyz(100.0, 0.0, 0.0),
        ))
        .add_child(enemy_dialogue)
        .id();

    coroutines.jobs.spawn(
        JobLocation::Local,
        cutscene(player, player_dialogue, enemy, enemy_dialogue),
    );
}

async fn cutscene(player: Entity, player_dialogue: Entity, enemy: Entity, enemy_dialogue: Entity) {
    move_character(player, vec2(-100.0, 0.0), 50.0).await;

    timed_dialogue(enemy_dialogue, "You dare challenge me, mortal?", 2.0).await;

    move_character(player, vec2(-15.0, 0.0), 100.0).await;

    timed_dialogue(player_dialogue, "I will defeat you!", 2.0).await;

    move_character(enemy, vec2(15.0, 0.0), 80.0).await;

    timed_dialogue(enemy_dialogue, "Prepare to meet your maker!", 2.0).await;

    join!(
        spawn(
            JobLocation::Local,
            move_character(player, vec2(-50.0, 0.0), 150.0),
        ),
        spawn(
            JobLocation::Local,
            move_character(enemy, vec2(50.0, 0.0), 150.0),
        )
    );

    join!(
        move_character(player, vec2(0.0, 0.0), 200.0),
        move_character(enemy, vec2(0.0, 0.0), 200.0)
    );

    join!(
        move_character(player, vec2(-150.0, 0.0), 200.0),
        move_character(enemy, vec2(-15.0, 0.0), 50.0),
    );

    timed_dialogue(player_dialogue, "Argh! Such pain!", 2.0).await;

    timed_dialogue(enemy_dialogue, "Victory is mine!", 2.0).await;

    timed_dialogue(player_dialogue, "You've won!", 1.0).await;
    timed_dialogue(player_dialogue, "I surrender...!", 1.0).await;
}

async fn show_dialogue(dialogue: Entity, content: impl ToString) {
    let world = world().await;
    let world = &mut *world.write().unwrap();
    let mut text = world.get_mut::<Text2d>(dialogue).unwrap();
    text.0 = content.to_string();
}

async fn hide_dialogue(dialogue: Entity) {
    let world = world().await;
    let world = &mut *world.write().unwrap();
    let mut text = world.get_mut::<Text2d>(dialogue).unwrap();
    text.0.clear();
}

async fn timed_dialogue(dialogue: Entity, text: impl ToString, duration: f32) {
    show_dialogue(dialogue, text).await;
    wait_secs(duration).await;
    hide_dialogue(dialogue).await;
}

async fn move_character(character: Entity, target: Vec2, speed: f32) {
    loop {
        {
            let world = world().await;
            let world = &mut *world.write().unwrap();
            let dt = world.resource::<Time>().delta_secs();

            let mut transform = world.get_mut::<Transform>(character).unwrap();
            let direction = (target - transform.translation.truncate()).normalize_or_zero();
            let distance = transform.translation.truncate().distance(target);

            let speed = speed * dt;
            if distance < speed {
                transform.translation = target.extend(transform.translation.z);
                break;
            } else {
                transform.translation += direction.extend(0.0) * speed;
            }
        }

        next_frame().await;
    }
}

The cutscene coroutine we run looks like this:

use bevy::{camera::ScalingMode, prelude::*};
use futures::join;
use moirai::{coroutine::spawn, job::JobLocation};
use moirai_bevy::{
    Coroutines, MoiraiPlugin,
    coroutine::{next_frame, wait_secs, world},
};

fn main() {
    let mut app = App::new();
    app.add_plugins((DefaultPlugins, MoiraiPlugin))
        .add_systems(Startup, setup);
    app.run();
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    coroutines: Res<Coroutines>,
) {
    let font = asset_server.load("noto.ttf");

    commands.spawn((
        Camera2d,
        Projection::Orthographic(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 500.0,
            },
            ..OrthographicProjection::default_2d()
        }),
    ));

    let player_dialogue = commands
        .spawn((
            Text2d::default(),
            TextFont {
                font: font.clone(),
                font_size: 20.0,
                ..Default::default()
            },
            TextLayout::new_with_justify(Justify::Center).with_no_wrap(),
            TextColor::WHITE,
            Transform::from_xyz(0.0, 50.0, 0.0),
        ))
        .id();

    let player = commands
        .spawn((
            Mesh2d(meshes.add(Circle::new(15.0))),
            MeshMaterial2d(materials.add(Color::linear_rgb(0.0, 0.47, 0.95))),
            Transform::from_xyz(-150.0, 0.0, 0.0),
        ))
        .add_child(player_dialogue)
        .id();

    let enemy_dialogue = commands
        .spawn((
            Text2d::default(),
            TextFont {
                font: font.clone(),
                font_size: 20.0,
                ..Default::default()
            },
            TextLayout::new_with_justify(Justify::Center).with_no_wrap(),
            TextColor::WHITE,
            Transform::from_xyz(0.0, 50.0, 0.0),
        ))
        .id();

    let enemy = commands
        .spawn((
            Mesh2d(meshes.add(Rectangle::new(30.0, 30.0))),
            MeshMaterial2d(materials.add(Color::linear_rgb(0.95, 0.16, 0.22))),
            Transform::from_xyz(100.0, 0.0, 0.0),
        ))
        .add_child(enemy_dialogue)
        .id();

    coroutines.jobs.spawn(
        JobLocation::Local,
        cutscene(player, player_dialogue, enemy, enemy_dialogue),
    );
}

async fn cutscene(player: Entity, player_dialogue: Entity, enemy: Entity, enemy_dialogue: Entity) {
    move_character(player, vec2(-100.0, 0.0), 50.0).await;

    timed_dialogue(enemy_dialogue, "You dare challenge me, mortal?", 2.0).await;

    move_character(player, vec2(-15.0, 0.0), 100.0).await;

    timed_dialogue(player_dialogue, "I will defeat you!", 2.0).await;

    move_character(enemy, vec2(15.0, 0.0), 80.0).await;

    timed_dialogue(enemy_dialogue, "Prepare to meet your maker!", 2.0).await;

    join!(
        spawn(
            JobLocation::Local,
            move_character(player, vec2(-50.0, 0.0), 150.0),
        ),
        spawn(
            JobLocation::Local,
            move_character(enemy, vec2(50.0, 0.0), 150.0),
        )
    );

    join!(
        move_character(player, vec2(0.0, 0.0), 200.0),
        move_character(enemy, vec2(0.0, 0.0), 200.0)
    );

    join!(
        move_character(player, vec2(-150.0, 0.0), 200.0),
        move_character(enemy, vec2(-15.0, 0.0), 50.0),
    );

    timed_dialogue(player_dialogue, "Argh! Such pain!", 2.0).await;

    timed_dialogue(enemy_dialogue, "Victory is mine!", 2.0).await;

    timed_dialogue(player_dialogue, "You've won!", 1.0).await;
    timed_dialogue(player_dialogue, "I surrender...!", 1.0).await;
}

async fn show_dialogue(dialogue: Entity, content: impl ToString) {
    let world = world().await;
    let world = &mut *world.write().unwrap();
    let mut text = world.get_mut::<Text2d>(dialogue).unwrap();
    text.0 = content.to_string();
}

async fn hide_dialogue(dialogue: Entity) {
    let world = world().await;
    let world = &mut *world.write().unwrap();
    let mut text = world.get_mut::<Text2d>(dialogue).unwrap();
    text.0.clear();
}

async fn timed_dialogue(dialogue: Entity, text: impl ToString, duration: f32) {
    show_dialogue(dialogue, text).await;
    wait_secs(duration).await;
    hide_dialogue(dialogue).await;
}

async fn move_character(character: Entity, target: Vec2, speed: f32) {
    loop {
        {
            let world = world().await;
            let world = &mut *world.write().unwrap();
            let dt = world.resource::<Time>().delta_secs();

            let mut transform = world.get_mut::<Transform>(character).unwrap();
            let direction = (target - transform.translation.truncate()).normalize_or_zero();
            let distance = transform.translation.truncate().distance(target);

            let speed = speed * dt;
            if distance < speed {
                transform.translation = target.extend(transform.translation.z);
                break;
            } else {
                transform.translation += direction.extend(0.0) * speed;
            }
        }

        next_frame().await;
    }
}