Closed7

ECS 実装のニッチな goodies

Rust の ECS 実装 toecs の開発メモです (現在、 main ブランチ投下前) 。

Validation of Entity

古い Entity に対する component 挿入などは、無効な操作として弾く必要がある。

  1. insertremove などの操作実行時に都度チェックする (hecs など)
  2. 有効な Entity を掴んだハンドル (builder) を返す (Bevy の EntityMut)

複数の component

Bevy の場合 (Bundle)

Bevy の Bundle は複数の Component に対して実装されている。また Component で構成された struct に対して Bundle マクロが定義されている:

#[derive(Bundle)]
struct MyBundle {
    a: ComponentA,
    b: ComponentB,
    c: ComponentC,
}

複数の Bundle を挿入する場合は、 builder (EntityMutEntityCommands) のメソッドチェインを利用する:

commands
    .spawn_bundle(SpriteSheetBundle {
        texture_atlas: texture_atlas_handle,
        transform: Transform::from_scale(Vec3::splat(6.0)),
        ..default()
    })
    .insert(AnimationTimer(Timer::from_seconds(0.1, true)));

toecs の場合 (ComponentSet)

  • 複数の component は ComponentSet (Bevy 同様)
  • 1 つの component は ComponentSet
  • 複数の component set は ComponentSet

複数の component set を挿入する場合は 1 回のメソッド呼び出しで済ませる (メソッドチェインは無い):

// component set の組は component set
let player = world.spawn((
    // component は component set
    mdl::Player::default(),
    // component set (`#[derive(ComponentSet)]`)
    mdl::ActorSet {
        body: mdl::Body {
            pos: player_pos,
            dir: mdl::Dir8::S,
            is_block: true,
        },
        actor: mdl::Actor {},
    },
    view::actor::ActorViewSet {
        node: ui::NodeSet::default(),
        sprite: ui::Sprite::from_img(img.clone()).with_origin([0.5, 1.0]),
        dir_anim: view::SpriteDirAnim::new(
            &img.get().unwrap(),
            2.0,
            view::actor::DirAnimKind::Dir8,
            mdl::Dir8::S,
        ),
        view_body: view::actor::ViewBody::new(),
    },
));

遅延実行の必要性について

System から World への書き込みを行う場合

bevysparsey では、 Entity の作成や component の挿入に &mut World を必要とする。

  • bevy の場合: component の挿入・削除には archetype ストレージの移動が必要
  • sparsey の場合: Layout に基づいてグルーピングされた component の並び替えが必要

World からデータを借りて component を作る場合などは &mut World を取れない (取ると危険) なため、止むなく遅延実行で &mut World を取る。

並列実行の場合

World への書き込みを並列で作成して、同期ポイントで World に適用する。並列実行における Entity の作成には atomic 命令を用いて、すぐに Entity を返す。この Entity は作られた時点では無効なハンドルだが、他のコマンドよりも先に Entity 作成コマンドが実行されるため、その他のコマンドが実行されるときには有効なハンドルになっている。

Command 以外の遅延実行

bevy rfc#16 のコメント によると、 Unity ECS は world をマージして変更を取り込む (挿入限定?) とか。

ユーザ引数を取る system

System はデータを自動的に World から借りてくるが、一部の引数は自分で指定したいことがある。

Const generics (Bevy が元ネタ)

world.run(render::map_layer<1, 3>);

World::borrow を活かす (Bevy が元ネタ)

pub fn ctrl_entity_system(
    entity: Entity,
    (input, vi, mut body, mut view_body, stage, interact, mut cutscene): (
        Res<Input>,
        Res<res::VInput>,
        CompMut<mdl::Body>,
        CompMut<view::actor::ViewBody>,
        Res<res::Stage>,
        Comp<mdl::Interact>,
        ResMut<CutsceneState>,
    ),
) { /* ~~~~ */ }

ctrl_entity_system(entity, world.borrow());

shipyard の場合

第一引数をユーザ引数、以降が BorrowWorld な関数は World::run_with_data(S, Data) で実行できる。ユーザ引数の方が少ないことが多く、とても良い API だと思う。ただし実装はやや大変。。

System の所有

ライフタイムを消す + .into_system() を推論させる

&'a T&'b T は別の型なので GAT が必要になる! この GAT は stable Rust でも擬似的に再現できる:

引数を取るシステムの所有

shipyard::World::run_with_data で実行できる関数を Box に入れたい。まず struct BoxArgSystem<Data, Ret> の形が考えられるが、これは参照を引数に取りたい場合を表せられない。結局、 struct BoxRefArgSystem<Data, Ret>struct BoxMutArgSystem みたいに 3 つに型を分けることになってしまう。ややこしい。

Data は常に 'static であるとするのが無難かもしれない。

Vec::swap_remove が便利

Vec からデータを削除しつつ、他アイテムの位置を (極力) 移動させたくない場合、 Vec::swap_remove を活用することができる。

さっき Entity の削除に欠陥を見つけて身悶えした。。早くリベースして ECS 本を書き直したい。

このスクラップは2ヶ月前にクローズされました
ログインするとコメントできます