【bevy_async_system】bevyでUniTaskライクな機能が使えるライブラリを製作しました。
リポジトリは以下になります。
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);
}
}
右のブロックの色がおかしいですが気にしないでください。
物体が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_systems
にUpdate
を指定してシステムを渡すと毎フレーム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
Discussion