🕊️

Rust Bevy ECS 入門

53 min read 1

はじめに

Rust 製のゲームエンジン bevy を使いこなすには、そのシステムの基礎をなす Bevy ECS の理解が必須になる。ここでは Bevy ECS の基本的な使い方をざっくりとまとめる。画面に何か描画するようなコードは一切出てこない。

Rust と Bevy のバージョンは下記を想定している。

  • Rust: 1.57.0
  • Bevy: 0.6.0

本稿で使用している機能の使用例はこちらにまとめてあるので、ご参考まで。

https://github.com/hideakitai/bevy_ecs_introduction

Bevy を使用するための環境構築については、下記を参照。

https://bevyengine.org/learn/book/getting-started/setup/

Bevy における基本的なプログラムの概要

まず、Bevy ECS を使用した App の全体像をざっくりと把握するため、Bevy Book の ECS ページの例を参考にしたものを掲載する。

https://bevyengine.org/learn/book/getting-started/ecs/
use bevy::prelude::*;

/// Components
/// Component を derive した struct や enum が Component として使用できる
#[derive(Component)]
struct Person;
#[derive(Component)]
struct Name(String);

/// Startup System
/// Rust の通常の関数が System として使用できる
/// App の開始時に一度だけ呼ばれる
/// Commands を使い、Person と Name を持った Entity を生成する
fn add_people(mut commands: Commands) {
    commands.spawn().insert(Person).insert(Name("Rust".to_string()));
    commands.spawn().insert(Person).insert(Name("Bevy".to_string()));
    commands.spawn().insert(Person).insert(Name("Ferris".to_string()));
}

/// System
/// Rust の通常の関数が System として使用できる
/// Query を使って Component を取得し、それに対して処理を行う
fn greet_people(query: Query<&Name, With<Person>>) {
    for name in query.iter() {
        println!("hello {}!", name.0);
    }
}

/// App に System を登録し、Bevy の App を構築して Run する
fn main() {
    App::new()
        .add_startup_system(add_people)
        .add_system(greet_people)
        .run();
}
$ cargo run
hello Rust!
hello Bevy!
hello Ferris!

Bevy ECS を構成する基本要素

ECS では、Entity/Component/System をもとにシステム全体が構成される。

  • Entity : ユニークな ID を持つ、Component の集合体
  • Component : Entity に付与されるデータ
  • System : 様々なデータにアクセスして処理を行う関数

Bevy Book の説明が端的でわかりやすかったため、日本語訳を引用する。

Bevy のすべてのアプリのロジックは、Entity Component System パラダイムを使用しており、しばしば ECS と略記されます。ECS は、プログラムをエンティティ、コンポーネント、システムに分割するソフトウェアパターンです。エンティティはユニークな「もの」で、コンポーネントのグループを割り当てられ、それをシステムを使って処理します。

例えば、あるエンティティは PositionVelocity のコンポーネントを持ち、別のエンティティは PositionUI のコンポーネントを持つかもしれません。システムは、特定のコンポーネントタイプのセットで実行されるロジックです。例えば、PositionVelocity コンポーネントを持つすべてのエンティティで実行される移動システムがあるとします。

ECS パターンは、アプリのデータやロジックをコアコンポーネントに分割することで、クリーンでデカップリングされた設計を促進します。また、メモリアクセスのパターンを最適化し、並列処理を容易にすることで、コードの高速化にも役立ちます。

Bevy - ECS の序文 (DeepL による翻訳)

Bevy ECS では、Entity/Component/System に加えて、下記の機能が重要な役割を果たす。

  • Resource : App の中でグローバルで唯一のデータ
  • Query : System でアクセスする Entity を取得する
  • Commands : Entity の生成・破棄、Component の追加・削除、Resource の管理等を行う
  • Event : System 同士でデータを送受信する

Component や System は、できる限り小さな粒度で実装することが望ましい。 これにより、Bevy の並列処理のアクセス競合をできる限り小さくでき、処理の高速化が可能となる。次節から、Bevy ECS を構成する要素とその使い方を端的にまとめていく。

App

  • Bevy を使用するために、必要な全要素を App へ登録する
  • System, Plugin, Resource, Event 等を登録する必要がある
  • Component は App に登録する必要はない
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)  // 標準的な Bevy の機能を追加
        .insert_resource(MyResource)  // Global で唯一な Resource を追加
        .add_event::<MyEvent>()       // Event を追加
        .add_startup_system(setup)    // 初期化時に一度だけ呼ばれる System
        .add_system(my_system)        // System を追加
        .run();
}

Entity

  • Entity はユニークな ID を持つオブジェクトで、複数の Component の集合体を表す
  • Entity は後述の Command を使って生成し、複数の Component を追加できる
Entiry の定義
struct Entity(u64);
fn add_entity(mut commands: Commands) {
    let entity = commands
        .spawn()                           // Entity の生成
        .insert(Person)                    // Person Component の追加
        .insert(Name("Bevy".to_string()))  // Name Component の追加
        .id();                             // Entity を取得

    println!("Entity ID is {}", entity.id());
}

Component

  • Component は Entity に付与されるプロパティを表す
  • Component トレイトを実装した structenum が使用できる
  • New Type Pattern でシンプルな型でも Component として使用することができる
  • 空の struct を使えば Query Filter で利用可能な Marker Component として利用できる
Component の例
// `Component` を実装した struct や enum が Component として使用可能
#[derive(Component)]
struct Position { x: f32, y: f32 }

// New Type を使って単純な String を Component として使える
#[derive(Component)]
struct PlayerName(String);

// 空の struct は Marker Component としても使える
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Enemy;
fn add_entities(mut commands: Commands) {
    // Player Entity を生成する
    commands
        .spawn()                                   // Entity を生成
        .insert(Player)                            // Player の Marker を追加
        .insert(Position::default())               // Position Component を追加
        .insert(PlayerName("Ferris".to_string())); // PlayerName を追加

    // Enemy Entity を生成する
    commands
        .spawn()                      // Entity を生成
        .insert(Enemy)                // Enemy の Marker を追加
        .insert(Position::default()); // Position Component を追加
}

Bundle

  • Bundle は複数の Component 持つ Entity のテンプレートである
  • Bundle として使用する structBundlederive する必要がある
  • Bundle に中を Bundle を含め、ネストさせることもできる (#[bundle] が必要)
  • 任意の Component のタプルも Bundle として解釈される
Bundle の例
/// Component を Bundle としてまとめて定義する
/// Bundle を定義するには derive(Bundle) が必要
#[derive(Default, Bundle)]
struct PlayerStatus {
    hp: PlayerHp,
    mp: PlayerMp,
    xp: PlayerXp,
}

/// Bundle は入れ子にすることもできる
#[derive(Default, Bundle)]
struct PlayerBundle {
    name: PlayerName,
    position: Position,
    _marker: Player,
    // Bundle を入れ子にするには #[bundle] が必要
    #[bundle]
    status: PlayerStatus,
}
fn add_entities(mut commands: Commands) {
    // Player Bundle を持った Entity を生成する
    commands.spawn_bundle(PlayerBundle::default());
    // Enemy の Entity を生成する
    // タプルで Component をまとめると Bundle と解釈される
    commands.spawn_bundle((Enemy, Position { x: -1.0, y: -2.0 }));
}

System

  • System は App の様々な要素にアクセスし、様々な処理を行う
  • System は Rust の通常の関数として定義する
  • System 関数を実行させるには、下記のように App に登録する必要がある
    • add_startup_system() : 起動時に一度だけ呼ばれる処理を登録する
    • add_system() : 毎フレーム呼ばれる処理を登録する
fn setup () {
    println!("Hello from setup");
}

fn hello_world () {
    println!("Hello from system");
}

fn main() {
    App::new()
        .add_startup_system(setup) // 起動時に一度だけ呼ばれる
        .add_system(hello_world) // 毎フレームよばれる
        .run();
}

System から様々なデータへのアクセス

  • System の引数として指定することで、様々なデータへアクセスし処理を行うことが可能
    • Res<T> ResMut<T> : Resource にアクセスする
    • Query : フィルタリングされた Entity の Component にアクセスする
    • Commands : Entity/Component/Resource を生成・削除する
    • EventWriter EventReader : Event を送受信する
  • System の引数には、任意の種類を可変長で指定可能 (デフォルト 16 個まで)
  • タプルで引数をグループ化/ネストすることで、この引数の数の制限を回避することも可能
fn my_system(
    resource: Res<MyResource>,        // ResourceA にイミュータブルアクセス
    query: Query<&MyComponent>,       // ComponentA をクエリしてアクセス
    mut commands: Commands,           // Commands を使って要素の生成・削除
    event_writer: EventWriter<Data>,  // Event を送信
    (ra, mut rb): (Res<ResourceA>, ResMut<ResourceB>),  // タプルでグループ化
) {
  // do something
}

System Order

  • Bevy はできる限り多くの System を並列して実行するように設計されている
  • 複数の System からデータへのミュータブルアクセスが必要な場合、実行順は保証されない
  • System Label を使用することで、明示的に System の実行順を制御することができる
App::new()
    // second という label をつける
    .add_system(second.label("second"))
    // first は second より前に
    .add_system(first.before("second"))
    // fourth は second より後に
    .add_system(fourth.label("fourth").after("second"))
    // third は second より後 fourth より前に
    .add_system(third.after("second").before("fourth"))
    .run();

System Set

  • System Set は複数の System にまとめて Label/Order/State 等を付与することができる
  • System Set 内の System に、個別に Label や Ordering を設定することもできる
App::new()
    // second と third をまとめて SystemSet にする
    .add_system_set(
        SystemSet::new()
            // label をつけておく
            .label("second and third")
            // first よりも後に second と third を実行する
            .after("first")
            // ここにも label をつけておく
            .with_system(second.label("second"))
            // third は second より後に
            .with_system(third.after("second")),
    )
    // first は second and third より前に
    .add_system(first.label("first").before("second and third"))
    // fourth は second and third より後に
    .add_system(fourth.after("second and third"))
    .run();

System Chaining

  • 複数の System の入出力を連結し、一つの大きな System として扱うことができる
  • これにより、System から Result<T> などのエラーを返すことができるようになる
  • Chain された System への入力は In(result): In<Result<()>> のように指定する
  • ただし Chain の最後の System は、出力 (返り値) を持つことはできない

Chain された System 内でのミュータブルアクセスは System の並列実行を妨げ、パフォーマンスの低下につながるので注意が必要

use anyhow::Result;
use bevy::prelude::*;

/// 処理の結果を後段の System へ転送する
fn parse_number() -> Result<()> {
    let s = "number".parse::<i32>()?; // Err
    Ok(())
}

/// 前段の System から送られてきた Result が Err だったら処理する
fn handle_error(In(result): In<Result<()>>) {
    if let Err(e) = result {
        println!("parse error: {:?}", e);
    }
}

fn main() {
    App::new().add_system(
            // chain() で後段の System を登録する
            parse_number.chain(handle_error),
        )
        .run();
}

SystemParam

  • SystemParam は System の引数をひとまとめにした型の定義
  • SystemParam は System の引数にできるものは何でも入れることができる
  • 定義する型には SystemParam トレイトを derive する必要がある
  • Bevy 0.6.0 時点では Lifetime は下記のように指定する必要がある
    • SystemParam を derive する際は <'w, 's> と指定する必要がある
      • 'w : World の Lifetime
      • 's : State の Lifetime
    • Query は <'w, 's>, Resource は <'w>, Local Resource は <'s> を要求する
    • Query 内の Component の Lifetime は 'static を要求する
    • メンバが 'w 's を要求しない場合は PhantomData を導入する必要がある
use bevy::ecs::system::SystemParam; // 要インポート

/// SystemParam は System の引数にできるものは何でも持つことができる
#[derive(SystemParam)]
struct MySystemParam<'w, 's> {
    query: Query<'w, 's, (Entity, &'static MyComponent)>,
    resource: ResMut<'w, MyResource>,
    local: Local<'s, usize>,
}

#[derive(SystemParam)]
struct MySystemParam2<'w, 's> {
    resource: ResMut<'w, MyResource>,
    // 's を満たすために PhantomData が必要
    #[system_param(ignore)]
    _secret: PhantomData<&'s ()>,
}

/// SystemParam は直接 System の引数にすることができる
fn query_system_param(mut my_system_param: MySystemParam) {
    // ..
}

Query

  • Query を使い、System から Entity の Component へアクセスすることができる
  • Query 結果には、条件にマッチした複数の Entity の Component が含まれる
    • iter() iter_mut() で Component へのイテレータを取得できる
    • get(entity) get_mut(entity) で指定した Entity の Component を取得できる
    • Entity がひとつだけなら、下記のように直接 Component を取得できる
      • single() single_mut(): ひとつだけでない場合は panic! する
      • get_single() get_single_mut(): ひとつだけかどうかの Result を返す
Query の基本的な使い方
fn my_system(mut q: Query<&mut MyComponent>) {
    // iter() iter_mut() でイテレートして処理
    for c in q.iter_mut() {
        // すべての MyComponent をミュータブルにイテレートして処理
    }

    // get() get_mut() で Entity を指定して取得
    if let Ok(c) = q.get_mut(entity) {
        // 指定した Entity が見つかれば処理
    }

    // Entity がひとつだけなのであれば、直接取得できる
    let c = q.single();
}

Query の様々な指定方法

  • Query に指定する Component の型は &T &mut T やその Option である必要がある
  • Bundle は Query に指定できず、Bundle に含まれる個別の Component を使う必要がある
  • タプルで複数の Component を指定すれば、そのすべてを持つ Entity を取得できる
  • Option<T> を使うことで、その Component が存在する場合は取得できる
  • Entity を Query に指定することで、Entity ID を取得できる
fn my_system(
    q_a: Query<&CompA>,                    // A を持つ Entity
    mut q_b: Query<&mut CompB>,            // B を持つ Entity (Mutable)
    q_ac: Query<(&CompA, &CompC)>,         // A と C を両方持つ Entity
    q_ad: Query<(&CompA, Option<&CompD>)>, // A と持っていれば D
    q_ae: Query<(&CompA, Entity)>,         // A とその Entity ID)
{
    for a in q_a.iter() {
        println!("{:?}", a);
    }

    let mut b = q_b.single_mut().unwrap();
    // do something with b
    println!("{:?}", b);

    for (a, c) in q_ac.iter() {
        println!("{:?}, {:?}", a, c);
    }

    for (a, d) in q_ad.iter() {
        println!("{:?}, {:?}", a, d);
    }

    for (a, e) in q_ae.iter() {
        println!("{:?}, {:?}", a, e);
    }
}

Query Filter

  • Query の第二引数を指定することで、Query 結果にフィルタかけることができる
  • Query Filter で指定した Component は Query の要素には含まれない
  • 複数の Query Filter をタプルでくくることで、AND 条件でフィルタリングができる
  • 複数の Query Filter を Or<(...)> でくくることで、OR 条件でフィルタリングができる
  • Query Filter には下記のような種類がある
    • With<T> Without<T> Or<(...)> Added<T> Changed<T>
fn my_system(
    q_a_wc: Query<&CompA, With<CompC>>,                       // A with C
    q_a_woc: Query<&CompA, Without<CompC>>,                   // A w/o C
    q_ac_wd: Query<(&CompA, &CompC), With<CompD>>,            // A + C with D
    q_a_wc_wod: Query<&CompA, (With<CompC>, Without<CompD>)>, // A with C && w/o D
    q_a_wc_wd: Query<&CompA, Or<(With<CompC>, With<CompD>)>>, // A with C || with D
) {
    // ...
}

Query Sets

  • 同じ Component を複数のフィルタでミュータブルに Query することは出来ない
  • QuerySetQueryState を使えば、4 つまで同じ型をミュータブルに Query 可能になる
  • q0() q1() のようにして、各 Query にアクセスすることができる
/// NG: ミュータブルな A の Query が複数存在する
fn my_system(
    mut q_a_wb: Query<&mut CompA, With<CompB>>, // A with B (Mutable)
    mut q_a_wc: Query<&mut CompA, With<CompC>>, // A with C (Mutable)
) {
    // ...
}

/// OK
fn my_system(
    mut q: QuerySet<(
        QueryState<&mut CompA, With<CompB>>, // A with B (Mutable)
        QueryState<&mut CompA, With<CompC>>, // A with C (Mutable)
    )>,
) {
    for mut a in q.q0().iter_mut() {
        // QueryState<&mut CompA, With<CompB>> にアクセス
    }

    for mut a in q.q1().iter_mut() {
        // QueryState<&mut CompA, With<CompC>> にアクセス
    }
}

その他の Query の機能

Query 結果を総当たりでイテレートする

  • 下記を使用すれば、その Query の複数個の組み合わせを総当たりできる
  • iter_combinations() iter_combinations_mut() でイテレータを生成
  • iter.fetch_next() で次の組を取得する (Option)
fn add_two_comp(query: Query<&MyComp>) {
    let mut iter = query.iter_combinations();
    while let Some([MyComp(c1), MyComp(c2)]) = iter.fetch_next() {
        println!("{} + {} = {}", c1, c2, c1 + c2);
    }
}
0 + 1 = 1
0 + 2 = 2
0 + 3 = 3
1 + 2 = 3
1 + 3 = 4
2 + 3 = 5

Parallel Iterator

  • ParallelIterator は重い Query の処理を並列で実行するイテレータ
  • ParallelIterator を使用するには、ComputeTaskPool が必要
  • par_for_each() par_for_each_mut() を Batch Size を指定して使用する
  • ParallelIterator は遅くなる可能性もあり、Profiling して採用を検討すべき
    • ParallelIterator はオーバーヘッドが大きい
    • 負荷の軽い処理や Batch Size が小さすぎる場合には遅くなる
use bevy::tasks::ComputeTaskPool; // 要 import

/// parallel_iterator を使用するには、ComputeTaskPool が必要
fn add_one(pool: Res<ComputeTaskPool>, mut query: Query<&mut MyComp>) {
    const BATCH_SIZE: usize = 100;
    query.par_for_each_mut(&pool, BATCH_SIZE, |mut my_comp| {
        my_comp.0 += 1;
    });
}

Resource

  • Resource は App で Global に共有される、唯一な存在のデータである (シングルトン)
参考: DefaultPlugins を入れた状態で登録されている Resource 一覧 (Windows)
  • ActiveCameras
  • AmbientLight
  • Arc<Queue>
  • AssetServer
  • AssetServerSettings
  • Assets<AudioSource>
  • Assets<ColorMaterial>
  • Assets<DynamicScene>
  • Assets<Font>
  • Assets<FontAtlasSet>
  • Assets<Gltf>
  • Assets<GltfMesh>
  • Assets<GltfNode>
  • Assets<GltfPrimitive>
  • Assets<Image>
  • Assets<Mesh>
  • Assets<Scene>
  • Assets<Shader>
  • Assets<StandardMaterial>
  • Assets<TextureAtlas>
  • AsyncComputeTaskPool
  • Audio
  • AudioOutput
  • Axis<GamepadAxis>
  • Axis<GamepadButton>
  • ClearColor
  • ComputeTaskPool
  • Diagnostics
  • DirectionalLightShadowMap
  • EventLoop<()>
  • EventLoopProxy<()>
  • Events<AppExit>
  • Events<AssetEvent<AudioSource>>
  • Events<AssetEvent<ColorMaterial>>
  • Events<AssetEvent<DynamicScene>>
  • Events<AssetEvent<Font>>
  • Events<AssetEvent<FontAtlasSet>>
  • Events<AssetEvent<Gltf>>
  • Events<AssetEvent<GltfMesh>>
  • Events<AssetEvent<GltfNode>>
  • Events<AssetEvent<GltfPrimitive>>
  • Events<AssetEvent<Image>>
  • Events<AssetEvent<Mesh>>
  • Events<AssetEvent<Scene>>
  • Events<AssetEvent<Shader>>
  • Events<AssetEvent<StandardMaterial>>
  • Events<AssetEvent<TextureAtlas>>
  • Events<CloseWindow>
  • Events<CreateWindow>
  • Events<CursorEntered>
  • Events<CursorLeft>
  • Events<CursorMoved>
  • Events<FileDragAndDrop>
  • Events<GamepadEvent>
  • Events<GamepadEventRaw>
  • Events<KeyboardInput>
  • Events<MouseButtonInput>
  • Events<MouseMotion>
  • Events<MouseWheel>
  • Events<ReceivedCharacter>
  • Events<TouchInput>
  • Events<WindowBackendScaleFactorChanged>
  • Events<WindowCloseRequested>
  • Events<WindowCreated>
  • Events<WindowFocused>
  • Events<WindowMoved>
  • Events<WindowResized>
  • Events<WindowScaleFactorChanged>
  • FixedTimesteps
  • FlexSurface
  • GamepadSettings
  • Gamepads
  • Gilrs
  • Input<GamepadButton>
  • Input<KeyCode>
  • Input<MouseButton>
  • IoTaskPool
  • LogSettings
  • Msaa
  • PointLightShadowMap
  • RenderDevice
  • SceneSpawner
  • ScratchRenderWorld
  • TextPipeline<Entity>
  • Time
  • Touches
  • TypeRegistryArc
  • VisiblePointLights
  • WgpuOptions
  • Windows
  • WinitWindows

Resource の追加と削除

  • Resource の追加には 3 通りの方法がある
    • App 初期化時に insert_resource() でインスタンスを追加
    • App 初期化時に init_resource::<T>() で初期化して追加 (要 Default or FromWorld)
    • System 内で Command を使って追加 (insert_resource() のみ)
  • Resource を削除するには System 内で Command を使い remove_resource::<T>() する
  • 既存の Resource をもう一度追加しようとすると、新たな値によって上書きされる
fn main() {
    App::new()
        // ...
        .init_resource::<MyResourceA>()   // Default or FromWorld で初期化されて追加
        .insert_resource(MyResourceB(5))  // 手動で初期化して追加
        // ...
        .run();
}

fn my_system(mut commands: Commands) {
    commands.insert_resource(MyResource(3));
    commands.remove_resource::<MyResource>();
}

Resource の初期化 (init_resource::<T>())

  • シンプルな初期化であれば Default を実装するだけで十分
  • FromWorld を実装すれば、World の全要素にアクセスしつつ複雑な初期化をすることが可能
/// Default で初期化される Resource
#[derive(Default)]
struct MyFirstCounter(usize);

/// FromWorld で初期化される Resource
struct MySecondCounter(usize);

impl FromWorld for MySecondCounter {
    // ここでは ECS World のすべての要素にアクセスすることが可能
    fn from_world(world: &mut World) -> Self {
        let count = world.get_resource::<MyFirstCounter>().unwrap();
        MySecondCounter(count.0) // MyFirstCounter の値で初期化
    }
}

/// 初期化できない Resource
struct MyThreshold(usize);

System から Resource へのアクセス

  • System からは Res<T> ResMut<T> を使って Resource にアクセスできる
  • Option でくるめば、存在しないかもしれない (今後追加/削除される) Resource を扱える
/// System
fn count_up(
    thresh: Res<MyThreshold>,                        // イミュータブルに参照
    mut first_counter: ResMut<MyFirstCounter>,       // ミュータブルに参照
    second_counter: Option<ResMut<MySecondCounter>>, // 存在するかわからない Resource
) {
    if let Some(mut second_counter) = second_counter {
        // MySecondCounter が存在したら何かする
    }
}

Time Resource

  • Bevy が提供する Time Resource を 使うと、ゲーム内の時間を追跡できる
  • Bevy が提供する Timer 型を使えば、特定の時間が経過したかを検出できる
use bevy::prelude::*;

#[derive(Component)]
struct MyTimer(Timer);

fn setup(mut commands: Commands) {
    // MyTimer を 2.0 秒ごとに繰り返すように設定
    commands.spawn().insert(MyTimer(Timer::from_seconds(2.0, true)));
}

// MyTimer に経過時間を Time Resource を使って適用し、指定時間が経過したかをチェックする
fn print_timer(time: Res<Time>, mut query: Query<&mut MyTimer>) {
    let mut my_timer = query.single_mut();
    if my_timer.0.tick(time.delta()).just_finished() {
        println!("Tick");
    }
}

fn main() {
    App::new()
        .add_plugins(MinimalPlugins)
        .add_startup_system(setup)
        .add_system(print_timer)
        .run();
}

Local Resource

  • Local<T> は各 System 内でのみ使用できる、可変な Local Resource
  • 同じ型 Local<T> でも複数の System で共有されず、各 System で別のインスタンスとなる
  • 自動的に初期化されるため、DefaultFromWorld を実装している必要がある
  • System を App に追加する際に、個別に初期化することも可能
use bevy::prelude::*;

/// Local<T> には Default の実装が必要
#[derive(Default)]
struct MyCounter(usize);

fn count_up_1(mut counter: Local<MyCounter>) {
    // ここの counter と
}
fn count_up_2(mut counter: Local<MyCounter>) {
    // ここの counter は別物
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_system(count_up_1)
        // .config() を使って手動で Local<MyCounter> を初期化する
        .add_system(count_up_2.config(|params| {
            params.0 = Some(MyCounter(0)); // 0 番目の引数を初期化したいので params.0
        }))
        .run();
}

Commands

  • Commands を使うことで、下記のようなことができる
    • Resource の追加・削除 (insert_resource(), remove_resource::<T>())
    • Entity (EntityCommands) の追加 (spawn(), spawn_bundle(), spawn_batch())
    • Entity (EntityCommands) の取得 (entity())
  • Entity (EntityCommands) を上記で取得し、Component/Bundle 等の追加・削除を行う
    • id() で Entity を取得
    • insert() insert_bundle() で Entity へ Component を追加
    • remove::<T>() remove_bundle::<T>() で Entity から Component を削除
    • despawn() despawn_recursive() で Entity を削除
  • Commands は即時に適用はされずキューに積まれ、次の Stage への遷移直前に実行される
fn setup(mut commands: Commands) {
    // Entity の生成と Component/Bundle の追加
    let a = commands
        .spawn()        // Entity (EntityCommands) を生成
        .insert(CompA)  // Component を追加
        .id();          // id() で Entity を取得できる
    let ab = commands
        .spawn_bundle((CompA, CompB))  // Bundle を持った Entity を生成
        .id();
    let abc = commands
        .spawn()
        .insert(CompA)
        .insert_bundle((CompB, CompC)) // Bundle を追加 (タプルは Bundle)
        .id();

    // spawn_batch() で Bundle へのイテレータから複数の Entity を一度に生成できる
    commands.spawn_batch(vec![
        (CompA, CompB, CompC),
        (CompA, CompB, CompC),
        (CompA, CompB, CompC),
    ]);

    // Entity の Component を削除
    commands.entity(abc).remove::<CompB>();

    // Entity の削除
    commands.entity(a).despawn();
    commands.entity(ab).despawn();
    commands.entity(abc).despawn();

    // Resource の追加と削除
    commands.insert_resource(MyResource);
    commands.remove_resource::<MyResource>();
}

Event

  • EventWriter<T> EventReader<T> を介して、任意のデータを System 間で送受信できる
  • Event はブロードキャストされ、受信側 System は独立して同じ Event を同時に処理する
  • Event は App の作成時に add_event::<T>() で追加する必要がある

受信側の System が送信側よりも早いタイミングで動作した場合、Event は 1 フレーム遅れて受信されることとなる。同じフレームに Event を受信したい場合は、System Ordering によって System の動作順序を明確に指定する必要がある。

use bevy::prelude::*;

/// 送受信する Event
/// 内部に送受信したい様々なデータをもたせる
struct MyEvent(String);

/// Event を送信する System
/// EventWriter を使って MyEvent を送信する
fn event_write(mut event_writer: EventWriter<MyEvent>) {
    event_writer.send(MyEvent("Hello, event!".to_string()))
}

/// Event を受信する System
/// EventReader のイテレータを使って受信 Event を処理する
fn event_read(mut event_reader: EventReader<MyEvent>) {
    for e in event_reader.iter() {
        let msg = &e.0;
        println!("received event: {}", msg);
    }
}

fn main() {
    App::new()
        .add_event::<MyEvent>() // Event を App に登録する必要がある
        .add_system(event_write)
        .add_system(event_read)
        .run();
}

Plugin

  • Plugin は App をモジュラーにリファクタリングする際に有用
  • Plugin は App に追加する要素をひとまとまりにすることができる
  • App を module に分割して Plugin を適用すれば、module 内で設計を完結できる
    • App に必要なすべての型を pub として外部に公開する必要がなくなる
  • Plugin の有用な使用例としては、下記のような例が挙げられる
    • Physics や外部入力など、様々なサブシステムを Plugin としてまとめる
    • Bevy の追加機能を自作して crate として公開する
    • 異なる State に対してそれぞれ別の Plugin を実装する
  • Plugin を使用するには、Plugin トレイトを実装する必要がある
use bevy::prelude::*;

/// Plugin にまとめたい様々な要素
struct MyResource;
struct MyEvent;

/// 自作の Plugin
struct MyPlugin;

/// 自作の Plugin に Plugin トレイトを実装すれば、Plugin として使用できる
/// Plugin トレイトでは App Builder に必要な要素を追加するだけで良い
impl Plugin for MyPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(MyResource)
            .add_event::<MyEvent>()
            .add_startup_system(plugin_setup)
            .add_system(plugin_system);
    }
}

/// Plugin にまとめたい startup_system
fn plugin_setup() {
    println!("MyPlugin's setup");
}

/// Plugin にまとめたい system
fn plugin_system() {
    println!("MyPlugin's system");
}

fn main() {
    App::new()
        .add_plugin(MyPlugin) // Plugin を追加する
        .run();
}

Plugin Group

  • Plugin Group は複数の Plugin を一度に登録することができる
  • DefaultPlugins MinimalPlugins なども Plugin Group として定義されている
  • Plugin Group を使用するには、PluginGroup トレイトを実装する必要がある
use bevy::app::PluginGroupBuilder; // PluginGroup トレイトを実装するには追加が必要
use bevy::prelude::*;

/// 自作の Plugin
struct FooPlugin;
struct BarPlugin;
impl Plugin for FooPlugin {}
impl Plugin for BarPlugin {}

/// 自作の Plugin Group
struct MyPluginGroup;

impl PluginGroup for MyPluginGroup {
    fn build(&mut self, group: &mut PluginGroupBuilder) {
        group.add(FooPlugin).add(BarPlugin); // Plugin を group に追加
    }
}

fn main() {
    App::new()
        .add_plugins(MyPluginGroup) // PluginGroup を追加
        .run();
}
  • DefaultPlugins MinimalPlugins では、下記の詳細に示すような様々な Plugin が導入される
  • DefaultPlugins はゲーム制作に必要な一通りの Plugin が導入される
  • MinimalPlugins は Window のない Headless App を構成する最低限の Plugin が導入される
    • DefaultPlugins 内の WinitPlugin がないと App は一度きりしか動かない
    • MinimalPlugins では ScheduleRunnerPlugin が代わりに Game Loop を構成する
`DefaultPlugins` `MinimalPlugins` で導入される Plugin の一覧
pub struct DefaultPlugins;

impl PluginGroup for DefaultPlugins {
    fn build(&mut self, group: &mut PluginGroupBuilder) {
        group.add(bevy_log::LogPlugin::default());
        group.add(bevy_core::CorePlugin::default());
        group.add(bevy_transform::TransformPlugin::default());
        group.add(bevy_diagnostic::DiagnosticsPlugin::default());
        group.add(bevy_input::InputPlugin::default());
        group.add(bevy_window::WindowPlugin::default());
        group.add(bevy_asset::AssetPlugin::default());
        group.add(bevy_scene::ScenePlugin::default());
        #[cfg(feature = "bevy_render")]
        group.add(bevy_render::RenderPlugin::default());
        #[cfg(feature = "bevy_sprite")]
        group.add(bevy_sprite::SpritePlugin::default());
        #[cfg(feature = "bevy_pbr")]
        group.add(bevy_pbr::PbrPlugin::default());
        #[cfg(feature = "bevy_ui")]
        group.add(bevy_ui::UiPlugin::default());
        #[cfg(feature = "bevy_text")]
        group.add(bevy_text::TextPlugin::default());
        #[cfg(feature = "bevy_audio")]
        group.add(bevy_audio::AudioPlugin::default());
        #[cfg(feature = "bevy_gilrs")]
        group.add(bevy_gilrs::GilrsPlugin::default());
        #[cfg(feature = "bevy_gltf")]
        group.add(bevy_gltf::GltfPlugin::default());
        #[cfg(feature = "bevy_winit")]
        group.add(bevy_winit::WinitPlugin::default());
        #[cfg(feature = "bevy_wgpu")]
        group.add(bevy_wgpu::WgpuPlugin::default());
    }
}
pub struct MinimalPlugins;

impl PluginGroup for MinimalPlugins {
    fn build(&mut self, group: &mut PluginGroupBuilder) {
        group.add(bevy_core::CorePlugin::default());
        group.add(bevy_app::ScheduleRunnerPlugin::default());
    }
}

Plugin Group の中の Plugin を一部無効化する

  • Plugin Group を使用する場合は、その中の一部の Plugin を無効化できる
  • 無効化されていてる Plugin でも、ビルドはされる
App::new()
    .add_plugins_with(DefaultPlugins, |plugins| {
        plugins
            .disable::<AudioPlugin>() // AudioPlugin を無効化
            .disable::<LogPlugin>() // LogPlugin を無効化
    })
    .run();

Bevy の Plugin を crate.io に公開する

  • Bevy の crate を公開する際にも Plugin/Plugin Group として公開するのが良い
  • Plugin を公開する際には、下記のような情報を参考に

https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md
https://bevy-cheatbook.github.io/setup/bevy-git.html#advice-for-plugin-authors
https://github.com/bevyengine/bevy-assets

State

  • State は App の実行時の流れを構造化し、制御することができる機能
    • メニュースクリーンやロードスクリーン
    • ゲームの Pause/Resume
    • 異なるゲームモード 等

State と System Set の登録

  • State を使用するには下記を実施する必要がある
    • State を定義する enum には Debug Clone PartialEq Eq Hash の実装が必要
    • add_state() で初期値の State を App Builder へ登録
    • State 専用の System Set を登録 (各 State に対して複数追加できる)
  • State の様々なタイミングのみで動作する System を追加することができる
    • 通常の State で使用するもの
      • on_enter : State 開始時に一度だけ呼ばれる
      • on_update : State の動作中に毎フレーム呼ばれる
      • on_exit : State の終了時 (遷移時) に一度だけ呼ばれる
    • State Stack に関連するもの (後述)
      • on_pause : State が Inactive になる際に一度だけ呼ばれる
      • on_in_stack_update : State が Inactive でも Active でも毎フレーム呼ばれる
      • on_inactive_update : State が Inactive なときだけ毎フレーム呼ばれる
      • on_resume : State が Active に戻る際に一度だけ呼ばれる
/// State は enum として定義する (Debug, Clone, PartialEq, Eq, Hash が必要)
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum AppState { Menu, Game, End }

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // State の初期値を登録して State を有効化する
        .add_state(AppState::Menu)
        // State に関係なく動作する System
        .add_system(count_frame)
        // AppState::Menu に入ったときのみ動作する System Set
        .add_system_set(
            SystemSet::on_enter(AppState::Menu).with_system(menu_on_enter)
        )
        // AppState::Game で毎フレーム動作する System Set
        .add_system_set(
            SystemSet::on_update(AppState::Game).with_system(game_on_update)
        )
        // AppState::End を終了するときのみ動作する System Set
        .add_system_set(
            SystemSet::on_exit(AppState::End).with_system(end_on_exit)
        )
        .run();
}

System から State へのアクセス

  • System 内で State へアクセスする場合 Res<State<T>> ResMut<State<T>> を使う
    • current() で現在の State を取得できる
    • set() で State を変更できるが、下記の場合は失敗する
      • 既にその State になっている場合
      • 他の State への変更が既にキューされている場合
/// System 内で State にアクセスするためには `State<T>` Resource を使う
fn count_frame(app_state: Res<State<AppState>>) {
    // current() で現在の State が取得できる
    match app_state.current() {
        // ...
    }
}

/// System 内で State にアクセスするためには `State<T>` Resource を使う
fn menu_on_enter(mut app_state: ResMut<State<AppState>>) {
    app_state.set(AppState::Game).unwrap(); // Menu から Game へ Statを を変更
}

State Stack

  • State を完全に移行せずにスタックさせることも可能 (Pause・メニュー画面などに使える)
  • State<T>push()/pop() メソッドを使って State Stack を利用できる
  • State がバックグラウンドになった Inactive 時に動作する System を登録することもできる
fn push_to_state_stack(mut app_state: ResMut<State<AppState>>) {
    // push() で State Stack に State を積む
    app_state.push(AppState::Paused).unwrap();
}

fn pop_from_state_stack(mut app_state: ResMut<State<AppState>>) {
    // pop() で State Stack から以前の状態に戻す
    app_state.pop().unwrap();
}

// ...

fn main() {
    App::new()
        // ...
        // AppState::Game が Inactive になる際に一度だけ呼ばれる
        .add_system_set(
            SystemSet::on_pause(AppState::Game)
                .with_system(game_on_pause)
        )
        // AppState::Game が Inactive でも Active でも毎フレーム呼ばれる
        // ただし Bevy 0.6.0 にはバグがあり、on_update() と同じ動作しかしてくれない
        // https://github.com/bevyengine/bevy/issues/3179
        .add_system_set(
            SystemSet::on_in_stack_update(AppState::Game)
                .with_system(game_on_in_stack_update),
        )
        // AppState::Game が Inactive なときだけ毎フレーム呼ばれる
        .add_system_set(
            SystemSet::on_inactive_update(AppState::Game)
                .with_system(game_on_inactive_update),
        )
        // AppState::Game が Active に戻る際に一度だけ呼ばれる
        .add_system_set(
            SystemSet::on_resume(AppState::Game)
              .with_system(game_on_resume)
        )
        // ...
        .run();
}
  • Input<T> をトリガとして State を変更する場合、input は自分でクリアする必要がある
  • 詳細はこちらの issue を参照
fn esc_to_menu(
    mut keys: ResMut<Input<KeyCode>>,
    mut app_state: ResMut<State<AppState>>,
) {
    if keys.just_pressed(KeyCode::Escape) {
        app_state.set(AppState::MainMenu).unwrap();
        keys.reset(KeyCode::Escape);  // you should clear input by yourself
    }
}
  • 当たり前だが、State が動作していない間に受け取った Event 検知されない
  • State は Run Criteria を用いて実装されており、Fixed Timestamp のような新しい Run Criteria を適用すると、System が State と連動しなくなる
  • Fixed Timestamp のような異なる Run Criteria と State との共存は不可能ではないが、トリッキーな方法が必要になる

Run Criteria

  • 特定の System を実行するか否かを Run Criteria と呼ばれる関数で実行時に判定する機能
  • 任意の条件が満たされた場合のみ動作する System, System Set, Stage を追加できる
  • Run Criteria は低レベルな機能で、State 等で制御しきれない場合に使用する

Run Criteria を適用した System

  • Run Criteria は enum ShouldRun を返す System として定義する
  • Run Criteria は通常の System と同様、任意のパラメータを引数にできる
use bevy::ecs::schedule::ShouldRun; // Run Criteria を使用するには追加が必要
use bevy::prelude::*;

/// count が 100 より大きくなったときのみ ShouldRun::Yes を返す Run Criteria
fn my_run_criteria(mut count: Local<usize>) -> ShouldRun {
    if *count > 100 {
        *count = 0;
        ShouldRun::Yes
    } else {
        *count += 1;
        ShouldRun::No
    }
}

/// my_run_criteria を適用する System
fn my_system() {
    println!("Hello, run criteria!");
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // with_run_criteria() で Run Criteria を System に適用する
        .add_system(my_system.with_run_criteria(my_run_criteria))
        .run();
}

Run Criteria Label

  • 同じ Run Criteria を共有する System/System Set がある場合、Label の使用が推奨される
  • Label 付き Run Criteria は各フレームで一度だけ実行され、結果が全 System で共有される
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_system(
            // my_run_criteria に Label をつけておく
            my_system.with_run_criteria(my_run_criteria.label("MyRunCriteria")),
        )
        // with_run_criteria に Label を指定すれば、その結果が使い回される
        .add_system_set(
            SystemSet::new()
                .with_run_criteria("MyRunCriteria")
                .with_system(my_system_a)
                .with_system(my_system_b),
        )
        .run();
}
  • 当たり前だが、ShouldRun::No のタイミングで受け取った Event は検知できない
  • State や Fixed Timestamp のような Run Criteria を使った機能を使用している場合、新たな Run Criteria を追加すると、その System の動作が置き換えられるので注意

Label

  • Label は System, Run Criteria, Stage, Ambiguity Sets に名前をつけるために使用する
  • Label には文字列はもちろん、独自の型も使用可能
  • 文字列ではなく独自の型を Label とすることで、型チェックや補完などの恩恵が得られる
  • 独自の Label 型には Clone + Eq + Hash + Debug (+ Send + Sync + 'static) の実装が必要
  • 独自の Label 型には、用途に応じて下記のいずれかのトレイトの実装が必要
    • StageLabel
    • SystemLabel
    • RunCriteriaLabel
    • AmbiguitySetLabel
/// 独自の Label 型を定義する
#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemLabel)]
struct MySecondSystemLabel;

// ...

fn main() {
    App::new()
        // 独自の Label 型は文字列の Label と同様に使用できる
        .add_system(second.label(MySecondSystemLabel))
        .add_system(first.before(MySecondSystemLabel))
        .add_system(third.after(MySecondSystemLabel))
        .run();
}

Stage

  • Bevy の 1 フレームは、デフォルトで 5 つの Stage (CoreStage) から構成されている
  • 下記の Stage が上から順に実行され、各 Stage で各々に登録された System が実行される
    • CoreStage::First
    • CoreStage::PreUpdate
    • CoreStage::Update : ユーザが追加した System はデフォルトでここに登録される
    • CoreStage::PostUpdate
    • CoreStage::Last
  • 次の Stage への移行時には、その Stage の全 System が実行済みであることが保証される
  • 各 Stage の System 内での Command は、全 System の実行完了後に実行される
  • 各 Stage 内の System は、デフォルトで可能な限り並列で実行される
  • Stage の追加は System の並列実行を妨げるため、System Ordering の使用が望ましい
  • Bevy の内部 System は CoreStage の合間で動作するため、不意の相互作用に注意

独自 Stage を利用すべき場合

  • 各 Stage のすべての System が実行された後、同じフレームで System を実行したい
  • 各 Stage のすべての Command が実行された後、同じフレームで System を実行したい

独自の Stage の使用方法

  • 独自の Stage とその Stage での System は下記のように追加することができる
    • 独自の Stage に Label をつけて登録する (Stage の場所は様々ある)
    • Stage の動作は single_threaded()parallel() かが指定できる
    • add_system_to_stage で Stage Label を指定して System を追加する
use bevy::prelude::*;

/// 独自の Stage Label を定義する
#[derive(Debug, Clone, PartialEq, Eq, Hash, StageLabel)]
struct MyStage;

// ...

fn main() {
    App::new()
        // 独自の Stage を Label をつけて登録する (SystemStage::parallel() も使用可能)
        .add_stage_after(CoreStage::Update, MyStage, SystemStage::single_threaded())
        // CoreStage::Update で実行される System を登録
        .add_system(first)
        // MyStage に System を登録
        .add_system_to_stage(MyStage, second)
        .add_system_to_stage(MyStage, third)
        .run();
}

Change Detection

Component Change Detection

  • Query Filter を使うことで、Component データの追加・変更を簡単に検知することができる
    • Added<T> : Component の新しいインスタンスの生成を検知する
      • Component を持った新しい Entity が生成されたとき
      • 既存の Entity に Component が追加されたとき
    • Changed<T> : Component のインスタンスに変更を検知する
      • Component を持った新しい Entity が生成されたとき (Added<T> と同じ)
      • Component がミュータブルアクセスされた場合
        • DerefMut によるため、ミュータブルに Query しただけでは起きない
        • DerefMut によるため、実際に値が変更されなくても検知される
  • ChangeTrackers<T> Component を Query することで、フィルタリングせずに検知できる
/// Added<T> を Query Filter として、追加された Component を Query
fn print_added(query: Query<(Entity, &Counter), Added<Counter>>) {
    for (entity, counter) in query.iter() {
        println!("Added counter {}, count =  {}", entity.id(), counter.0);
    }
}

/// Changed<T> を Query Filter として、変更された Component を Query
fn print_changed(query: Query<(Entity, &Counter), Changed<Counter>>) {
    for (entity, counter) in query.iter() {
        println!("Changed counter entity {} to {}", entity.id(), counter.0);
    }
}

/// ChangeTrackers<T> を Query すれば、フィルタリングせずに追加・変更を検知できる
fn print_tracker(query: Query<(Entity, &Counter, ChangeTrackers<Counter>)>) {
    for (entity, counter, trackers) in query.iter() {
        if trackers.is_added() {
            println!("Tracker detected addition {}, {}", entity.id(), counter.0);
        }
        if trackers.is_changed() {
            println!("Tracker detected change {} to {}", entity.id(), counter.0);
        }
    }
}

Resource Change Detection

  • Resource の追加・変更の検知は、Res/ResMut のメソッドによって検知できる
    • is_added() : Resource が追加されたかどうか
    • is_changed() : Resource が変更されたかどうか
/// 存在するかわからない Resource の追加と変更を検知する
fn print_added_changed(counter: Option<Res<Counter>>) {
    if let Some(counter) = counter {
        // Counter が追加されたときに実行される
        if counter.is_added() {
            println!("Counter has added");
        }
        // Counter が変更されたときに実行される
        if counter.is_changed() {
            println!("Counter has change to {}", counter.0);
        }
    } else {
        println!("Counter has not found");
    }
}

Removal Detection

Component Removal Detection

  • Component の削除の検知は、Stage をまたいだ処理をすることで可能
    • Command を使用した Component の削除は各 Stage の最後に行われるため
  • RemovedComponents<T> が引数の System を後段の Stage に登録し、イテレートすれば良い
/// CoreStage::PostUpdate で MyComponent の削除を検知する
/// RemovedComponents<T> でこれまでに削除された Component を検出できる
fn detect_removals(removals: RemovedComponents<MyComponent>) {
    for entity in removals.iter() {
        println!("MyComponent Entity {} has removed", entity.id());
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // Component を追加する
        .add_startup_system(add_components)
        // CoreStage::Update で MyComponent が存在するときは削除する
        .add_system(remove_components_if_exist)
        // CoreStage::PostUpdate で MyComponent の削除を検知する
        .add_system_to_stage(CoreStage::PostUpdate, detect_removals)
        .run();
}

Resource Removal Detection

  • Bevy から Resource の削除を検知するための API は提供されない
  • Option で Resource を包み、Local<T> で前フレームでの有無を保持すれば検知できる
  • この仕組み上、Resource の削除の検知は必ず 1 フレーム遅れる
/// 前フレームで Resource が存在したかを保存することで、次のフレームで削除を検知する
fn detect_removals(
    my_resource: Option<Res<MyResource>>,  // 存在するかわからないので Option
    mut my_resource_existed: Local<bool>,  // 前フレームでの有無をローカルに保存
) {
    if let Some(_) = my_resource {
        println!("MyResource exists");
        *my_resource_existed = true;
    } else if *my_resource_existed {
        println!("MyResource has removed");
        *my_resource_existed = false;
    }
}

Hierarchical Entity (Parent/Child)

  • Command を使用することで、Entity の親子関係をつくることができる (子は 8 個まで)
  • 主な利用方法は、ゲーム内の Transform に親子関係を作ること (ここでは言及しない)
    • 自作オブジェクトに親子関係を持たせても、そのままでは移動に親子関係を持たない
    • GlobalTransform Transform の両方を親子に追加すれば、移動に親子関係ができる

Parent/Child Entity の構築

  • Entity に親子関係を作る方法は、下記の 2 通りがある
    • spawn() 時に with_children() で Child をつくる
    • Entity を取得して push_children() する
#[derive(Component)]
struct MyParent(String);
#[derive(Component)]
struct MyChild(String);

fn setup(mut commands: Commands) {
    let parent = commands
        .spawn()
        .insert(MyParent("MyParent".to_string())) // Entity を MyParent とし、
        .with_children(|parent| {
            // Child として MyChild を生成
            parent.spawn().insert(MyChild("MyChild1".to_string()));
        })
        .id();

    // 別途 MyChild Entity を生成し
    let child = commands
        .spawn()
        .insert(MyChild("MyChild2".to_string()))
        .id();

    // MyParent の Entity ID を使って MyChild を Child として追加
    commands.entity(parent).push_children(&[child]);
}

Parent/Child Entity へのアクセス

非常に紛らわしいが、親子関係を作ることで下記のように Component が付与される。

  • Child となった Entity には Parent Component が付与される
  • Parent となった Entity には Children Component が付与される

そのため、それぞれを Query するためには下記のようにする。

  • Parent Entity を Query するには Query<&Children> を使う
  • Child Entity を Query するには Query<&Parent> を使う

また、Parent Children Component には、それぞれの子・親を特定するため、

  • 子が持つ Parent Component は親の Entity ID を .0 で参照できる
  • 親が持つ Children Component は子の Entity ID の集合をイテレートできる

これらを用いて、実際の Entity (下記の例では MyParentMyChild) を取得するには、

  • 親は Parent の Entity ID を使い、親 MyParent の Query から取得する
  • 子は Children 内の Entity ID を使い、子 MyChild の Query から特定の子を取得する
/// Child Entity の Query から、それぞれの Parent Entity を取得する
fn find_parent_from_children(
    q_child: Query<&Parent>,       // Parent Component を持つ Child Entity を Query
    q_my_parent: Query<&MyParent>, // MyParent を Query
) {
    // Child Entity が持っている Parent Component をイテレート
    for parent in q_child.iter() {
        // Parent Component は Entity (ID) を 0 番目の要素として持つので、
        // それを使って q_my_parent から MyParent を取得する
        let p = q_my_parent.get(parent.0).unwrap();
        println!("{}", p.0);
    }
}

/// Parent Entity の Query から、Children Entity を取得する
fn find_children_from_parent(
    q_parent: Query<&Children>,  // Children Component を持つ Parent を Query
    q_my_child: Query<&MyChild>, // MyChild を Query
) {
    // Parent Entity が持っている Children Component をイテレート
    for children in q_parent.iter() {
        // Children Component は Child Entity ID の集合を持っているのでイテレート
        for &child in children.iter() {
            // Child Entity ID を使って、q_my_child から MyChild を取得する
            let my_child = q_my_child.get(child).unwrap();
            println!("{}", my_child.0);
        }
    }
}

Parent/Child Entity の破棄

  • Command で親の Entity ID を指定し、子を含めて再帰的に despawn することができる
/// MyParent の Entity ID を Query し、それで MyParent を MyChild 含めて再帰的に破棄する
/// MyParent は Children を持っているので、それを Query Filter として使う
fn clean_up(mut commands: Commands, query: Query<Entity, With<Children>>) {
    let e = query.single().unwrap();        // MyParent は唯一のはず
    commands.entity(e).despawn_recursive(); // MyParent を MyChild 含めて再帰的に破棄
}

Custom Runner

  • App を Update する Runner を独自に定義することができる
  • DefaultPlugins MinimalPlugins の Runner は上書きされるので注意
use bevy::prelude::*;

/// App を手動で Update する Runner をカスタマイズできる
fn my_runner(mut app: App) {
    println!("my_runner!");
    app.update();
}

fn hello_world() {
    println!("Hello, world!");
}

fn main() {
    App::new()
        // Custom Runner を適用する
        .set_runner(my_runner)
        .add_system(hello_world)
        .run();
}

System を指定した間隔で実行する

  • System を指定した間隔で実行するには、いくつかの方法がある
    • FixedTimestep の Run Criteria を使う (System ごとに指定)
    • ScheduleRunnerPlugin を使う (全 System)

Fixed Timesetp (Run Criteria)

  • Bevy 組み込みの Run Criteria の一つに FixedTimestep がある
  • 指定した Time Step が経過した場合のみ System が実行される
use bevy::core::FixedTimestep; // 要 import
use bevy::prelude::*;

fn hello_world() {
    println!("hello world");
}

fn main() {
    App::new()
        .add_plugins(MinimalPlugins)
        .add_system_set(
            SystemSet::new()
                // FixedTimestep を Run Criteria として設定する
                // Time Step (== Frame Duration) を 0.5 に設定 (2 FPS)
                .with_run_criteria(FixedTimestep::step(0.5))
                .with_system(hello_world),
        )
        .run();
}

ScheduleRunnerPlugin (Runner)

  • ScheduledRunnerPlugin で全 System を一定周期で実行できる
  • MinimalPlugins には同梱されている (winit の代わりの Runner)
  • Runner がコンフリクトするため DefaultPlugin (winit) とは共存できない
/// 要 import
use bevy::app::{ScheduleRunnerPlugin, ScheduleRunnerSettings};

fn main() {
    App::new()
        // 1 秒ごとに System が実行されるように設定し、Plugin を導入
        .insert_resource(ScheduleRunnerSettings::run_loop(Duration::from_secs_f64(
            1.0,
        )))
        .add_plugin(ScheduleRunnerPlugin::default())
        .add_system(hello_world)
        .run();
}

Exclusive System (Direct World/ECS Access)

  • exclusive_system() を使用することで、System から直接 World にアクセスが可能
  • &mut world: World を引数とする System を作り、exclusive_system() として登録する
  • Exclusive System は App のスレッドで動作するスレッドローカルな System となる
  • Exclusive System 実行中は他の System の並行動作が止まるので使用を控えるのが望ましい
  • World を使えばあらゆることが可能だが、詳細は docs を参照

https://docs.rs/bevy/latest/bevy/ecs/world/struct.World.html
/// Exclusive System を使うことで、App 内の World のすべての要素にアクセスできる
/// https://docs.rs/bevy/latest/bevy/ecs/world/struct.World.html
fn my_exclusive_system(world: &mut World) {
    println!("Here is my exclusive system");
    world.insert_resource(MyResource);
    world.spawn().insert(MyComponent);
}

fn main() {
    App::new()
        .add_system(my_exclusive_system.exclusive_system())
        .run();
}

Thread Pool Resource

  • DefaultTaskPoolOptions::with_num_threads(n) でスレッドプールの数を変更できる
  • 上記データを Resource として追加する
/// スレッドプールの数を指定することができる
fn main() {
    App::new()
        .insert_resource(DefaultTaskPoolOptions::with_num_threads(4))
        .add_plugins(DefaultPlugins)
        .run();
}

Ambiguity Detection of System Execution Order

  • System は並列で実行されるため、登録順と実行順が前後する可能性がある
  • 同じデータを扱う System を順序指定なしで実行すれば、出力は状況によって変わり得る
  • 下記 Plugin を使用 System 順と実行順の入れ替りを検知してログを出力できる
    • ReportExecutionOrderAmbiguities
    • LogPlugin (DefaultPlugin に同梱)
  • これは確実な検知ではないため、あくまで参考情報として使用する
App.new()
    // ...
    .add_plugin(LogPlugin::default())  // DefaultPlugin には同梱
    .insert_resource(ReportExecutionOrderAmbiguities)
    .run();
Execution order ambiguities detected, you might want to add an explicit dependency relation between some of these systems:
 * Parallel systems:
 -- "&app::increment_counter" and "&app::print_counter"
    conflicts: ["Counter"]

Writing Tests for Systems

  • System のテストは Rust 標準の cargo test を使用して実施できる
  • テスト内で World を自分で作り、様々な要素を追加し、実行すれば良い
    • Entity や Resource を生成する
    • Stage をつくって System を登録する
    • System を実行し、結果を取得して確認する
tests/test_system.rs
//! cargo test を使って、System のテストを実行することが可能
//! cargo test --test test_system

use bevy::prelude::*;

struct Counter(usize);

/// テストされる System
fn count_up(mut query: Query<&mut Counter>) {
    for mut counter in query.iter_mut() {
        counter.0 += 1;
    }
}

#[test]
fn has_counted_up() {
    // World を自分で作り、Counter を持った Entity を生成する
    let mut world = World::default();
    let entity = world.spawn().insert(Counter(0)).id();

    // Stage を自分で作り、そこにテストすべき System を追加して実行する
    let mut update_stage = SystemStage::parallel();
    update_stage.add_system(count_up);
    update_stage.run(&mut world);

    // Component を取得して結果をテストする
    let count = world.get::<Counter>(entity).unwrap();
    assert_eq!(count.0, 1);
}

References

https://bevyengine.org/learn/book/introduction/
https://bevy-cheatbook.github.io/introduction.html
https://github.com/bevyengine/bevy/tree/main/examples/ecs
https://docs.rs/bevy/latest/bevy/index.html
https://bevyengine.org/news/bevy-0-6/
https://bevyengine.org/learn/book/migration-guides/0.5-0.6/
https://bevyengine.org/news/bevy-0-5/
https://bevyengine.org/learn/book/migration-guides/0.4-0.5/

Discussion

いい記事ありがとうございます!!!

bevyいじっているときに、このあたりの基本的なコンポーネントの説明がなくて困っていたので助かりました!

ログインするとコメントできます