chip13. ECSの"System"に値を渡したい
はじめに
2023/09/17時点の内容です。
- rustc 1.72.0
- bevy 0.11.2
bevyは開発初期段階のOSSで、まだまだ破壊的なアップデートが入ります。
でも、面白いですよ。
Bevy0.7の頃からある「値を渡す方法」
以下はBevy公式HPのLearnの中にあるMigration Guide: 0.6 to 0.7のサンプルコードです。
// 0.7
fn local_is_42(local: u32) -> impl FnMut() {
// This closure will be the system that will be executed
move || {
assert_eq!(local, 42);
}
}
fn main() {
App::new().add_system(local_is_42(42)).run();
}
自分流にサンプルコード書くとこんな感じでしょうか。
use bevy::prelude::*;
use std::fmt::Display;
fn main()
{ App::new()
.add_systems( Startup, my_system( "Hello, world!" ) ) //引数による値わたし
.add_systems( Startup, my_system( 10 ) ) //引数による値わたし
.run();
}
fn my_system<T: Display>( value: T ) -> impl FnMut()
{ move || { println!( "{value}" ); }
}
この構文のミソはAppビルダーの中にあるmy_system( … )
の書き方です。いかにも引数でSystemへ値を渡しているように見えます。
しかしこれ、実はSystemに当たるのはfn my_system( … )
ではなく、その返り値であるmove || { print!( "{value}" ); }
の方ですよね。fn my_system( … )
は、Systemになるクロージャを返す普通の関数です。
一見いい感じに見える構文ですが、しかしこの方法だとBevy ECSの要であるSystemParamsを利用できない制限があるようです。例えばクロージャを、
move |x:Local<MyStruct>| { print!( "{value}" ); }
などと書けないかな?と試しましたがダメでした。エラー表示によるとクロージャの引数はゼロ個で固定だとか。
error[E0593]: closure is expected to take 0 arguments, but it takes 1 argument
--> chips\src\main.rs:12:41
|
12 | fn my_system<T: Display>( value: T ) -> impl FnMut()
| ^^^^^^^^^^^^ expected closure that takes 0 arguments
13 | { move |x:Local<MyLocal>| { print!( "{value}" ); }
| ----------------------- takes 1 argument
Bevyの将来のバージョンで改良されたりしないかしら? (^_^;)
違う方法を考える
似たようなことを別の手段で実現することを考えます。
1. Resourceを使う
BevyのResourceはグローバル変数のように扱えます。System側がどのResourceを使うかは、Resourceにセットされた値の型によって決まります。この方法ならfn my_system( … )
は名実ともにSystemであり、SystemParamsの利用に制限はありません。
use bevy::prelude::*;
use std::fmt::Display;
#[derive( Resource )]
struct MyRes<T> ( T ); //Resourceの定義
fn main()
{ App::new()
.insert_resource( MyRes ( "Hello, world!" ) ) //Resourceに値をセット
.insert_resource( MyRes ( 10i32 ) ) //Resourceに値をセット
.add_systems( Startup, my_system::<&str> ) //型の指定付きSystem登録
.add_systems( Startup, my_system::<i32> ) //型の指定付きSystem登録
.run();
}
fn my_system<T: Send + Sync + 'static + Display> ( value: Res<MyRes<T>> )
{ println!( "{}", value.0 );
}
Resourceの仕組み上 型と値は一対一でないといけないから、同じ型の値が複数必要な場合は、ニュータイプパターンで型の種類を増やす必要があります。
上記サンプルコードはジェネリクスを使っているせいでSend + Sync + 'static
の明示が必要でした。ちょっと記述がくどく感じられますね。
2. 型とトレイトでなんとかしてみる
そして「Rustなんだから型とトレイトで何とかすればいいじゃない」という脳筋的発想はコチラ。
.add_systems( Startup, my_system::<MyType> )
と書きたいが為にボイラープレートを追加し、さらに値の設定がimplの方へ移動してしまっているという本末転倒ぶり。
ジェネリクスで書けなかったし(&strのところね)‥‥これはダメな例ですね (^_^;) 。
use bevy::prelude::*;
//型とトレイトのかたまり
struct MyType ( &'static str );
trait MyTrait
{ fn getter( &self ) -> &str;
}
impl MyTrait for MyType
{ fn getter( &self ) -> &str { self.0 }
}
impl Default for MyType
{ fn default() -> Self { Self ( "Hello, world!" ) } //値がこんな場所に
}
fn main()
{ App::new()
.add_systems( Startup, my_system::<MyType> ) //こう書きたかった
.run();
}
//ジェネリクスの受け皿としてLocalを利用する。
//Localの中身に対して「.0」は使えないのでgetter生やしてアクセス。
fn my_system<T: Send + Sync + Default + MyTrait>( value: Local<T> )
{ println!( "{}", value.getter() );
}
うん、やっぱりResourceを使うのが一番いいと思う _(:3 」∠)_ 。
Discussion