🐤

【bevy_async_system】bevyでUniTaskライクな機能が使えるライブラリを製作しました。

2023/10/16に公開

リポジトリは以下になります。
https://github.com/elmtw/bevy_async_system

https://crates.io/crates/bevy_async_system

Bevy で何かしらの処理や状態を待機するには主にステートやイベントが使用されます。例えば 2D の物体を一定の距離まで移動させるには次のようなコードが考えられます。

ステートを使用して単純な移動を待つ
use std::default::Default;

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_state::<MoveState>()
        .add_systems(Startup, setup)
        .add_systems(Update, move_shape.run_if(in_state(MoveState::Move)))
        .run();
}

#[derive(Component)]
struct Movable;

#[derive(States, Eq, PartialEq, Hash, Debug, Default, Copy, Clone)]
enum MoveState {
    #[default]
    Move,
    Finished,
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());

    commands.spawn((
        Movable,
        SpriteBundle {
            sprite: Sprite {
                custom_size: Some(Vec2::new(50., 50.)),
                color: Color::BLUE,
                ..default()
            },
            ..default()
        }
    ));
}

fn move_shape(
    mut shapes: Query<&mut Transform, With<Movable>>,
    mut state: ResMut<NextState<MoveState>>,
    time: Res<Time>,
) {
    for mut transform in shapes.iter_mut() {
        transform.translation.y += time.delta_seconds() * 30.;

        if Vec3::new(0., 80., 0.).distance(transform.translation) < 0.1 {
            state.set(MoveState::Finished);
        }
    }
}

上記のコードはまだそこまでのコード量ではありませんが、条件が複雑になると一気に肥大化、複雑化してしまいます。
例えば移動の数を 2 つにして両方の移動を待機するようにすると以下のような感じになります。

2 つの物体の移動を待機するように変更

use std::default::Default;

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_state::<MoveState>()
        .add_systems(Startup, setup)
        .add_systems(Update, (
            move_shape,
            move_finish
        ).run_if(in_state(MoveState::Move)))
        .run();
}


#[derive(Component)]
struct Movable;


#[derive(Component)]
struct TargetPos(Vec3);


#[derive(States, Eq, PartialEq, Hash, Debug, Default, Copy, Clone)]
enum MoveState {
    #[default]
    Move,
    Finished,
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());

    commands.spawn((
        Movable,
        TargetPos(Vec3::new(0., 80., 0.)),
        SpriteBundle {
            sprite: Sprite {
                custom_size: Some(Vec2::new(50., 50.)),
                color: Color::BLUE,
                ..default()
            },
            ..default()
        }
    ));

    commands.spawn((
        Movable,
        TargetPos(Vec3::new(80., 80., 0.)),
        SpriteBundle {
            sprite: Sprite {
                custom_size: Some(Vec2::new(50., 50.)),
                color: Color::GREEN,
                ..default()
            },
            transform: Transform::from_xyz(80., 0., 0.),
            ..default()
        }
    ));
}


fn move_shape(
    mut commands: Commands,
    mut shapes: Query<(Entity, &mut Transform, &TargetPos), (With<Movable>, With<TargetPos>)>,
    time: Res<Time>,
) {
    for (entity, mut transform, target_pos) in shapes.iter_mut() {
        transform.translation.y += time.delta_seconds() * 30.;

        if target_pos.0.distance(transform.translation) < 0.1 {
            commands.entity(entity).remove::<TargetPos>();
        }
    }
}


fn move_finish(
    mut state: ResMut<NextState<MoveState>>,
    moved_shapes: Query<&Movable, Without<TargetPos>>,
) {
    if moved_shapes.iter().len() == 2 {
        state.set(MoveState::Finished);
    }
}

move
右のブロックの色がおかしいですが気にしないでください。

物体が1つ増えただけですが、かなり複雑というか無理やりなコードになってしまいました。

このような待機を行う処理は移動のほかにも音楽の再生やアニメーションなどが考えられますが、どれも Bevy で実装するには 1 工夫必要です。

bevy_async_systemは主にこれらの待機を容易に行うことを目的として製作しました。
先ほどの 2 つの物体の移動の待機は、このライブラリを使用することでもっと簡潔に書けます。

bevy_async_system を使用して 2 つの物体の移動を待機
use std::default::Default;
use bevy::prelude::*;
use futures::future::join_all;
use futures::FutureExt;
use bevy_async_system::prelude::*;

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            AsyncSystemPlugin
        ))
        .add_systems(Startup, setup_async_systems)
        .run();
}


#[derive(Component)]
struct Movable;

#[derive(Component)]
struct Movable2;


fn setup(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());

    commands.spawn((
        Movable,
        SpriteBundle {
            sprite: Sprite {
                custom_size: Some(Vec2::new(50., 50.)),
                color: Color::BLUE,
                ..default()
            },
            ..default()
        }
    ));

    commands.spawn((
        Movable2,
        SpriteBundle {
            sprite: Sprite {
                custom_size: Some(Vec2::new(50., 50.)),
                color: Color::GREEN,
                ..default()
            },
            transform: Transform::from_xyz(80., 0., 0.),
            ..default()
        }
    ));
}


fn setup_async_systems(
    mut commands: Commands
) {
    commands.spawn_async(|schedules| async move {
        schedules.add_system(Update, once::run(setup)).await;
        // 物体1の移動タスク
        let move_handle1 = schedules.add_system(Update, wait::until(move_up::<Movable>));
        // 物体2の移動タスク
        let move_handle2 = schedules.add_system(Update, wait::until(move_up::<Movable2>));

        // 物体1と2の移動タスクをjoin allでawaitすることで
        // 両物体の移動が完了されるまで待機されます。
        join_all(vec![move_handle1.boxed(), move_handle2.boxed()]).await;
    });
}

fn move_up<C: Component>(
    mut shapes: Query<&mut Transform, With<C>>,
    time: Res<Time>,
) -> bool {
    let mut transform = shapes.single_mut();
    transform.translation.y += time.delta_seconds() * 10.;

    (80. - transform.translation.y).abs() < 0.01
}

コード中でライブラリを使用している箇所がsetup_async_systems内になります。
この関数内ではまず、spawn_asyncというライブラリから提供されたトレイトメソッドを使用し async なタスクを構築しています。
API の詳細は後程解説しますが、この中で物体の移動タスクをそれぞれの物体毎に生成しています。返り値にはタスクのハンドルが返され、これを待機することで物体の移動の完了を待ちます。
タスク自体は通常の future であるため、勿論 join_all などの引数として渡せます。
コード内でも join_all を使用しており、2 つの物体の移動が完了するまで待機しています。

このように、このライブラリを使用することで何らかの待機を行う処理が簡潔に書くことが出来るようになります。

非同期システムについて

AsyncSchedules::add_systemにはスケジュールラベルとスケジュールコマンド(仮)を渡します。

それぞれの引数について注意点があります。
まずスケジュールラベルですが、通常のApp::add_systemsとは挙動が異なります。
例えばApp::add_systemsUpdateを指定してシステムを渡すと毎フレームUpdateのタイミングでシステムが実行されますが、AsyncSchedules::add_systemではその限りではありません。
AsyncSchedules::add_systemの場合、システムの終了条件などを渡す必要がありますのでシステムそのものを渡すわけではなく、非同期スケジュールコマンド(仮)と自分が名付けている抽象化されたオブジェクトを通してシステムとタスクが生成されます。

これらのスケジュールコマンドの一覧は次の通りです。

once

once はシステムを一度だけ実行した後にタスクとシステムを完了させます。
このコマンドを生成するメソッドはいくつかあり、単純に渡されたシステムを実行するonce::runや、イベントを送信するonce::sendなどがあります。

下記に一覧を記載します。

fn setup(mut commands: Commands) {
    commands.spawn_async(|schedules| async move {
        schedules.add_system(Update, once::run(println_system)).await;
        schedules.add_system(Update, once::set_state(ExampleState::Second)).await;
        schedules.add_system(Update, once::init_resource::<Count>()).await;
        schedules.add_system(Update, once::init_non_send_resource::<NonSendCount>()).await;

        let count = schedules.add_system(Update, once::run(return_count)).await;
        schedules.add_system(Update, once::insert_resource(count)).await;
        schedules.add_system(Update, once::run(println_counts)).await;

        schedules.add_system(Update, once::send(AppExit)).await;
    });
}

fn println_system() {
    println!("hello!");
}

fn return_count() -> Count{
    Count(30)
}

fn println_counts(
    count: Res<Count>,
    non_send_count: NonSend<NonSendCount>
){
    println!("{count:?}");
    println!("{non_send_count:?}");
}

wait

waitはある条件が達成されるまでシステムを毎フレーム実行し続けます。
例えばwait::untilは bool を返すシステムを受けとり、trueになるまでシステムを実行し続けます。

一覧を以下に記載します。

fn output_methods(mut commands: Commands) {
    commands.spawn_async(|schedules| async move {
        // カウントが2になるまで実行
        schedules.add_system(Update, wait::until(|mut count: Local<u8>| {
            *count += 1;
            *count == 2
        })).await;

        // eventが来るまで待機
        schedules.add_system(Update, wait::until_event::<AppExit>()).await;

        // Someが返されるまで待機
        // Someの中身は返り値として返されます。
        let count = schedules.add_system(Update, wait::output(|mut count: Local<u8>| {
            *count += 1;
            if *count == 2{
                Some(*count)
            }else{
                None
            }
        })).await;
        assert_eq!(count, 2);

        // until_eventと違いイベントが返り値として返されます。
        let app_exit = schedules.add_system(Update, wait::output_event::<AppExit>());
    });
}

delay

delayはタスクを遅延させる処理を提供します。システムそのものを渡すわけではなく遅延するフレーム数やDurationを渡します。


fn setup(
    mut commands: Commands,
    mut settings: ResMut<FramepaceSettings>,
) {
    settings.limiter = Limiter::from_framerate(30.);
    commands.spawn_async(|schedules| async move {
        // 3秒間待機
        schedules.add_system(Update, delay::timer(Duration::from_secs(3))).await;

        // 90フレーム待機
        schedules.add_system(Update, delay::frames(90)).await;
    });
}

repeat

repeatを使うことでシステムを任意の回数実行し続けることが出来ます。
また、repeat::foreverを使うことで永久に(タスクが破棄されるまで)実行し続けるシステムも構築できます。

fn setup(mut commands: Commands) {
    commands.spawn_async(|schedules| async move {
        // 5回実行します。
        schedules.add_system(Update, repeat::times(5, count_up)).await;
        // 永久に実行し続けるシステムを生成します。
        let handle = schedules.add_system(Update, repeat::forever(count_up));
        // `repeat::forever`から生成されたタスクに限ったことことではありませんが、
        // タスクのハンドルをdropするとシステムも停止します。
        drop(handle);
    });
}

fn count_up(mut count: Local<u32>) {
    *count += 1;
    println!("count = {}", *count);
}

Exampleはここにあるので気になる方はこちもご参照ください。

bevy-akashicの宣伝

まだリリースはしていませんが、ニコ生ゲームをbevyを使って作るためのライブラリを製作中です。
もうじき出来上がる予定ですが、出来上がったら記事にする予定のためそちらもよろしくお願いします。

Akashic Engine
https://akashic-games.github.io/akashic-engine/v3/

Discussion