🦀

chip13. ECSの"System"に値を渡したい

2023/09/17に公開

はじめに

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のサンプルコードです。

Remove the config api
// 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();
}

自分流にサンプルコード書くとこんな感じでしょうか。

自分流Hello, world!
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の利用に制限はありません。

Resourceを使う
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