Rustで書かれたゲームエンジンamethyst勉強記 - System編

<< [#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](https://docs.rs/specs/0.14.3/specs/))が提供しているものですが）`System`トレイトを実装した独自の構造体を定義します。

```rust
struct ExampleSystem;

impl<'s> System<'s> for ExampleSystem {
    // ...
}
```

`System`トレイトは実装時に、関連型である`SystemData`と`run`メソッドを定義する必要があります。ここが**つまづきポイント**です。

## `SystemData`

関連型`SystemData`には、`SystemData`トレイト（同じ名前で紛らわしいですがこちらはトレイトです）を実装している構造体、もしくは構造体の組み合わせで表されるタプルを指定します。

基本的には`ReadStorage`, `WriteStorage`, `Read`, `Write`構造体の4つを利用します。

例として、`Transform`コンポーネントを与えた`Character`構造体を呼び出し、移動させる（座標を変化させる）処理を実装しようと思います。

```rust
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`トレイトを実装します。

```rust
struct Character;

impl Component for Character {
    type Storage = DenseVecStorage<Self>;
}
```

関連型`Storage`にはいくつか指定できる種類がありますが、基本的には`DenseVecStorage<Self>`で問題ありません（詳しくは英語で書かれた[ドキュメント](https://amethyst.rs/book/latest/concepts/component.html#storages)を読んでください）。

`~~Storage`で読み込むためには、エンティティを追加する処理が必要になります。前回触れた`create_entity()`, `with()`, `buind()`を利用します。

```rust
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`メソッドの実装です。

```rust
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つあります。
　１．エンティティを取得し、それに対応したコンポーネントを取得する方法（`HashMap`の`get()`のような）
　２．コンポーネントの組み合わせを指定し、それらを持つエンティティについてのコンポーネントのみを取得する方法
今回は２の方法について解説します。

`~~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`については、サンプルコードを用意して近いうちに別に解説しようと思います。

今回の記事についてのサンプルコードはありませんが、次回以降の記事ではここで解説した機能を活用していきます。
なにかわからないこと、わかりづらいことがありましたらコメントにて指摘していただけると幸いです。