Rustでオブジェクト指向的にプログラムを構築するのにちょっと難しさを感じて、ECSという手法を見てみた。 借用の被りを回避できるのはいいんだけど、システムで扱うコンポーネントが増えるに従ってストレージの引数が増えていくのがなかなか辛いと感じた。
これは試したSpecsというライブラリの問題なのかもしれない、と思い別のライブラリも見てみることにした。 AmethystというゲームエンジンがSpecsから載せ替えを計画しているという、Legionというライブラリを見てみた。
使用したバージョン: Legion 0.3.1
追記: Legin自体がどうも開発がストップしたっぽく、ECSも更新されなくなった…。
Legionの特徴
v0.3の説明にベンチマークで速いとかいろいろ書いてある。 性能面での比較はさておき、使用の際のSpecsに対する利点としては、
- システムでコンポーネントを扱う際に個々のストレージとしてじゃなく、 必要なコンポーネントのストレージを含むサブワールドとして受け取るので、 記述がそんなに増えずに済む
- エンティティやコンポーネントの生成・削除をコマンドバッファ経由で行うので、 その際に扱うコンポーネントのストレージを要求せずに済む
- システムごとに型を定義する必要がなく、関数に
#[system]
アトリビュートを指定するだけで作れる(扱うコンポーネントを2度書かずに済む) - コンポーネント、リソースに
Default
実装が必要ない
以下に逆引き:
コンポーネント定義
任意の型が自動的にコンポーネントとして使える。
use legion::*; |
エンティティの生成、削除
システム外:ワールドに push
pub fn init(world: &mut World) { |
システム内:コマンドバッファに push
|
- 複数のコンポーネントを持たせる場合、タプルで渡す
- スケジュール実行後、またはスケジュール中の
flush()
で実行される?
コンポーネントの追加、削除
// 追加 |
システム(for_each)
#[system(for_each)]
で、関数の引数に指定した型のコンポーネントを持つエンティティが自動的に列挙されて呼び出される:
#[system(for_each)] |
&Entity
で対応するエンティティを受け取れる- その他、リソースやサブワールドやコマンドバッファも受け取れる
コンポーネントの列挙
単純なループでは済まないような場合(二重ループなど)、システムでサブワールドを受け取って自分でループさせる:
#[system] |
#[read_component(型)]
または#[write_component(型)]
アトリビュートを指定すると、それらを含むSubWorld
として受け取れる- クエリのイテレータ:
<(型, 型, ...)>::query().iter(world)
、ミュータブルの場合はiter_mut
Entity
指定でエンティティが得られる
サブワールドの分割
サブワールドに対してクエリを行うと借用されてしまい他の操作ができなくなってしまうので、 複数のコンポーネントを別々に触りたい場合には分割する必要がある:
let (mut target, mut rest) = world.split::<&mut Position>(); |
- タプルの第1項目に指定したコンポーネント、第2項目に残りのコンポーネントを含むサブワールドに分割されて返される
- イミュータブルの場合には2番目のサブワールドでも参照可能
エンティティのコンポーネント取得
他のエンティティのコンポーネントを参照したい場合、クエリしてゲット
let (pos, vel) = <(&mut Position, &mut Velocity)>::query().get_mut(world, entity).unwrap(); |
- エントリ経由だと1つのコンポーネントにしか触れなかった…
リソースの登録(システム外)
let mut resources = Resources::default(); |
システムでのリソースの受け取り
システムの引数に #[resource]
を指定:
|
システム外でのリソース取得
let res = resources.get_mut::<リソース型>().unwrap(); |
Legionの使用感
Specsに比べてLegionではストレージを個別に扱わずに済むので、ある程度記述が楽になる。 ではいいこと尽くめで利点だけかといわれるとそうでもない:
- サブワールドに含まれないコンポーネントのクエリがコンパイル時にエラー検出できず、実行時に
panic!
する - システム間の依存関係が指定できず、スケジュールに
flush()
を挟む必要がある - エンティティやコンポーネントの生成・削除がコマンドバッファ経由で呼び出し時点では変更されないため、 状態にずれが生じて問題が出るケースがあるのではないか?
- 「システム内でこのコンポーネントにもアクセスしたい」と変更になった場合には同様に大幅な書き換えが必要
- システムの並列化に関して:多くのシステムで同じリソースを触ることになって、結局あまり並列化されないんじゃないかと疑っている
特にシステムにどのコンポーネントが必要かというのをシステム内から呼び出している関数を全部調べないといけないのは痛くて、 その点では自分でストレージを受け渡さないとコンパイルエラーが出る(し不要なものはワーニングが出る)Specsのほうがいいかもしれない。 ま、魔法のような方法はないということで…。