<< [#2 画像の表示](https://qiita.com/9laceef/items/383ca6525a4125f089eb) [#3 マウス入力と座標変更](https://qiita.com/9laceef/items/5b39fcf5e6fd6340ec78) >>
ご注意!
この記事の内容は2019/06/17現在の最新版amethystクレート(v0.11.0)では仕様が変わっており動きません!随時更新していきますので、しばしお待ちください。
もし更新が待てなかったり直接尋ねたいことなどありましたらコメントにてお願いいたします。
はじめに
今回は、新しくSystemというものを利用していきます。これはEcsモデルを利用した、amethystの提供するシステムの中でも極めて重要なものと思います。
自分がamethystを利用した際に「Unityっぽい」という感想を持ったのですが、まさしくこれがそうだと思います。Unityを触ったことのない人にとってはちんぷんかんぷんな話ではあると思いますが、Unityで利用するMonoBehaviour派生のスクリプトは、実装したUpdate()がフレームごとに呼び出されていますね。amethystのこのSystemも似たような実装を提供しています。
論よりコード
この言葉、友達が使っていたんですが語呂がいいですね。
System
amethystが提供する(正確にはamethystが利用しているクレート(=>specs)が提供しているものですが)Systemトレイトを実装した独自の構造体を定義します。
struct ExampleSystem;
impl<'s> System<'s> for ExampleSystem {
// ...
}
Systemトレイトは実装時に、関連型であるSystemDataとrunメソッドを定義する必要があります。ここがつまづきポイントです。
SystemData
関連型SystemDataには、SystemDataトレイト(同じ名前で紛らわしいですがこちらはトレイトです)を実装している構造体、もしくは構造体の組み合わせで表されるタプルを指定します。
基本的にはReadStorage, WriteStorage, Read, Write構造体の4つを利用します。
例として、Transformコンポーネントを与えたCharacter構造体を呼び出し、移動させる(座標を変化させる)処理を実装しようと思います。
struct ExampleSystem;
impl<'s> System<'s> for ExampleSystem {
type SystemData = (
ReadStorage<'s, Character>,
WriteStorage<'s, Transform>,
Read<'s, Time>
);
// ...
}
前述の4つの構造体は、Systemトレイトと同じライフタイム、必要とする構造体の型を指定する必要があります。またここで指定できる構造体にも条件があります。
ReadStorage, WriteStorageで指定する構造体はComponentトレイトを実装している必要があります。またReadStorageは指定した構造体の不変参照、WriteStorageは可変参照を得ることができますが、不変参照と可変参照を同時に得ることはできません。
Read, Writeは特別なトレイトの実装は必要ありません。~~Storageと同じようにそれぞれ不変参照と可変参照を得ることができますが、使い方に違いがあります。
Component
これらで指定するためにComponentトレイトを実装します。
struct Character;
impl Component for Character {
type Storage = DenseVecStorage<Self>;
}
関連型Storageにはいくつか指定できる種類がありますが、基本的にはDenseVecStorage<Self>で問題ありません(詳しくは英語で書かれたドキュメントを読んでください)。
~~Storageで読み込むためには、エンティティを追加する処理が必要になります。前回触れたcreate_entity(), with(), buind()を利用します。
struct ExampleState;
impl SimpleState for ExampleState {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
let world = data.world;
init_camera(world);
world.register::<Character>();
transform = Transform::default();
transform.set_xyz(100.0, 100.0, 0.0);
world
.create_entity()
.with(Character)
.with(transform)
.build();
}
}
これは前回に定義したExampleState構造体です。とりあえずon_start()の中でエンティティを登録します。
with()で独自のコンポーネントを与えるには、先にregister()で登録しておく必要があります。
そのあとは前回の通りwith()でエンティティにコンポーネントを与えます。このメソッドにはComponentトレイトを実装しているものしか渡すことができません。Transform構造体については前回触れていましたね。座標などを管理しているコンポーネントになっており、これは元からComponentトレイトが実装されています。
ReadStorage, WriteStorage
さて、ここまででようやく~~Storageを使う準備が整いました。
次はrunメソッドの実装です。
struct ExampleSystem;
impl<'s> System<'s> for ExampleSystem {
type SystemData = (
ReadStorage<'s, Character>,
WriteStorage<'s, Transform>,
Read<'s, Time>
);
fn run(&mut self, (characters, mut transforms, time): Self::SystemData) {
for (_character, transform) in (&characters, &mut transforms).join() {
transform.move_right(1.0 * time.delta_seconds());
}
}
}
第2引数の型はSelf::SystemDataになります。WriteStorageとWriteはmutキーワードを付ける必要があります。
ここからが難しいポイントです。
~~Storageから利用したいエンティティのコンポーネントを得る方法は大まかに2つあります。
1.エンティティを取得し、それに対応したコンポーネントを取得する方法(HashMapのget()のような)
2.コンポーネントの組み合わせを指定し、それらを持つエンティティについてのコンポーネントのみを取得する方法
今回は2の方法について解説します。
~~Storageの不変or可変参照(もしくはそのタプル)に対してjoin()を適用すると、それらに指定しているコンポーネントをすべて持つエンティティのコンポーネントへの、参照のタプルのイテレータを得ることができます。
例えば上記の場合だとCharacter, Transformコンポーネントの両方をもつエンティティだけが対象になります。Transformコンポーネントは持っていてもCharacterコンポーネントを持たないエンティティへの参照は含まれません。
Systemを追加する
これまで、GameDataBuilderを作成しwith_bundle()を適用していました。Systemを実装した構造体を登録するにはwith()を用います。
let game_data = GameDataBuilder::new()
// ... with_bundle()など
.with(ExampleSystem, "example-system", &[]);
第1引数は登録する構造体のインスタンスです。
第2引数はシステムを識別するための一意の文字列です。同じ識別名がすでに与えられていた場合パニックします。空文字("")は2回以上与えてもパニックしませんが、次に説明する第3引数での指定ができません。
第3引数は依存関係です。与えるシステム構造体が実行される前に、実行が完了していなければならないシステムの識別名のリストです。まだ試してはいませんが、これによりシステムの実行順を制御できるのかもしれません。
おわりに
Read, Writeについては、サンプルコードを用意して近いうちに別に解説しようと思います。
今回の記事についてのサンプルコードはありませんが、次回以降の記事ではここで解説した機能を活用していきます。
なにかわからないこと、わかりづらいことがありましたらコメントにて指摘していただけると幸いです。