<< [#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
については、サンプルコードを用意して近いうちに別に解説しようと思います。
今回の記事についてのサンプルコードはありませんが、次回以降の記事ではここで解説した機能を活用していきます。
なにかわからないこと、わかりづらいことがありましたらコメントにて指摘していただけると幸いです。