Rustで書かれたゲームエンジンamethyst勉強記#4

<< [#3 マウス入力と座標変更](https://qiita.com/9laceef/items/5b39fcf5e6fd6340ec78)　　[#5 キーボード入力](https://qiita.com/9laceef/items/6909e0b5633938667af4) >>

# ご注意！
この記事の内容は2019/06/17現在の最新版amethystクレート(v0.11.0)では仕様が変わっており動きません！随時更新していきますので、しばしお待ちください。
もし更新が待てなかったり直接尋ねたいことなどありましたらコメントにてお願いいたします。

### はじめに

これまではプログラムの開始時にのみエンティティを作成していました。しかしゲーム開発において動的にエンティティの作成・削除をしたい場面も多くあると思います。今回は左クリックでエンティティ作成、右クリックで削除するプログラムを作ってみます。またリソース、バンドルについても触れていきます。

サンプルコードは[こちら](https://github.com/glaceef/amethyst-test)。今回は`examples/04_create_and_destroy`になります。

# `main.rs`

## `lib.rs`からインポート

これらのプロジェクトは`amethyst_test`という名前のライブラリの`examples`に置いてあり、毎回書くのが面倒だったりするコードを`src/lib.rs`にまとめています。今回からそれらをインポートして使っていきますが、初めて使うものに関しては今まで通り解説を加えていきます。

```rust
use amethyst_test::{
    TransformExt,
    initialise_camera,
    load_sprite_sheet,
    mouse::*
};
```

### `TransformExt`トレイト

いつも`Transform`コンポーネントを作成する際、`Transform::default()`から座標を与えていましたが、このトレイトは`Transform::from_xyz()`関数を実装し、座標パラメータからコンポーネントを作成できます。

### `initialise_camera`関数

これは#2,#3で書いていたものになります。`&mut World`とウィンドウのサイズの配列`[f32; 2]`を引数にカメラコンポーネントを作成・追加します。

### `load_sprite_sheet`関数

こちらは#3で書いていたものになります。`&mut World`と画像のパスとスプライトシートのパスを与えることで`SpriteSheet`オブジェクトを返します。

### マウスシステム・リソース（`mouse::*`） 

\#3では`InputHandler`を用いてマウスの入力を取得していました。しかしながら、ボタンが押されているかは判定できても、「今ボタンが押されたか」は取得できません（`get`メソッドと`get_down`メソッドのようなものですね）。なので、`get` `get_down` `get_up`メソッドを実装した`Mouse`構造体を定義して、リソースとして呼び出すことでマウスボタンの状態を取得できるようにしています。詳しくはソースコードを見てください。

ではメインのプログラムに戻ります。

```rust
struct Icon {
    id: u32,
    dx: f32,
    dy: f32,
}
impl Icon {
    fn new(id: u32) -> Self {
        use rand::Rng;
        let mut rng = rand::thread_rng();
        Icon{
            id,
            dx: rng.gen_range(-3.0, 3.0),
            dy: rng.gen_range(-3.0, 3.0),
        }
    }
}
impl Component for Icon {
    type Storage = DenseVecStorage<Self>;
}

struct ExampleState;

impl SimpleState for ExampleState {
    fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
        let world = data.world;
        initialise_camera(world, [500.0, 500.0]);
        initialise_mouse(world);
        world.register::<Icon>();

        let sprite_sheet = load_sprite_sheet(
            world,
            "icon.png",
            "spritesheet.ron"
        );
        let sprite_render = SpriteRender {
            sprite_sheet: sprite_sheet,
            sprite_number: 0,
        };
        world.add_resource(sprite_render);
    }

    fn handle_event(
        &mut self,
        _: StateData<'_, GameData<'_, '_>>,
        event: StateEvent,
    ) -> SimpleTrans {
        if let StateEvent::Window(e) = event {
            if is_key_down(&e, VirtualKeyCode::Escape) {
                return Trans::Quit;
            }
        }
        Trans::None
    }
}

struct CreateDestroySystem(u32);

impl<'s> System<'s> for CreateDestroySystem {
    type SystemData = (
        Entities<'s>,
        WriteStorage<'s, Icon>,
        WriteStorage<'s, SpriteRender>,
        WriteStorage<'s, Transform>,
        ReadExpect<'s, SpriteRender>,
        Read<'s, Mouse>
    );

    fn run(
        &mut self,
        (entities,
         mut icons, mut sprite_renders, mut transforms,
         sr, mouse): Self::SystemData
    ) {
        // create
        if mouse.get_down(MouseButton::Left) {
            let id = { self.0 += 1; self.0 };
            let transform = Transform::from_xyz(250.0, 250.0, 0.0);
            entities.build_entity()
                .with(Icon::new(id), &mut icons)
                .with(sr.clone(), &mut sprite_renders)
                .with(transform, &mut transforms)
                .build();
        }

        // destroy
        if mouse.get_down(MouseButton::Right) && self.0 > 0 {
            let search_id = self.0;
            for (entity, icon) in (&entities, &icons).join() {
                if icon.id == search_id {
                    entities.delete(entity).unwrap();
                    self.0 -= 1;
                    break;
                }
            }
        }
    }
}

struct MoveSystem;

impl<'s> System<'s> for MoveSystem {
    type SystemData = (
        WriteStorage<'s, Icon>,
        WriteStorage<'s, Transform>,
    );

    fn run(&mut self, (mut icons, mut transforms): Self::SystemData) {
        let mut amount = 0;
        for (icon, transform) in (&mut icons, &mut transforms).join() {
            let (x, y) = {
                let translation = transform.translation();
                (translation.x, translation.y)
            };
            let (next_x, next_y) = (x + icon.dx, y + icon.dy);
            if next_x < 25.0 || 475.0 <= next_x {
                icon.dx = -icon.dx;
            }
            if next_y < 25.0 || 475.0 <= next_y {
                icon.dy = -icon.dy;
            }
            transform.translate_xyz(icon.dx, icon.dy, 0.0);
            amount += 1;
        }
        print!("\ritem amount: {}", amount);
    }
}

fn main() -> amethyst::Result<()> {
    // amethyst::start_logger(Default::default());

    let pipe = Pipeline::build().with_stage(
        Stage::with_backbuffer()
            .clear_target([1.0; 4], 1.0)
            .with_pass(DrawFlat2D::new().with_transparency(
                ColorMask::all(),
                ALPHA,
                Some(DepthMode::LessEqualWrite)
            ))
    );
    let config = DisplayConfig::load("./examples/04_create_and_destroy/config.ron");
    let render_bundle = RenderBundle::new(pipe, Some(config));

    let transform_bundle = TransformBundle::new();

    let input_bundle = InputBundle::<String, String>::new();
    let mouse_bundle = MouseBundle::new();

    let game_data = GameDataBuilder::new()
        .with_bundle(render_bundle.with_sprite_sheet_processor())?
        .with_bundle(transform_bundle)?
        .with_bundle(input_bundle)?
        .with_bundle(mouse_bundle)?
        .with(CreateDestroySystem(0), "create-destroy-system", &[])
        .with(MoveSystem, "move-system", &[]);

    Application::new(
        "./examples/04_create_and_destroy/",
        ExampleState,
        game_data
    )?.run();

    Ok(())
}
```

ずいぶん長くなって来ましたね。ただ説明のためなので`lib.rs`に移したコード以外はモジュール分けはせず一つにまとめています。

## `struct Icon`

```rust
struct Icon {
    id: u32,
    dx: f32,
    dy: f32,
}
impl Icon {
    fn new(id: u32) -> Self {
        use rand::Rng;
        let mut rng = rand::thread_rng();
        Icon{
            id,
            dx: rng.gen_range(-3.0, 3.0),
            dy: rng.gen_range(-3.0, 3.0),
        }
    }
}
impl Component for Icon {
    type Storage = DenseVecStorage<Self>;
}
```

前回とは異なり、`Icon`構造体がフィールドを持っています。もちろんStateやSystemもフィールドを持たせることができます。`Icon`構造体のインスタンスはそれぞれユニークなIDとx,y方向へのランダムな移動量を持っています。

## `struct CreateDestroySystem`

名前のダサさについては暖かい目で見てやってください。

```rust
struct CreateDestroySystem(u32);

impl<'s> System<'s> for CreateDestroySystem {
    type SystemData = (
        Entities<'s>,
        WriteStorage<'s, Icon>,
        WriteStorage<'s, SpriteRender>,
        WriteStorage<'s, Transform>,
        ReadExpect<'s, SpriteRender>,
        Read<'s, Mouse>
    );

    fn run(
        &mut self,
        (entities,
         mut icons, mut sprite_renders, mut transforms,
         sr, mouse): Self::SystemData
    ) {
        // create
        if mouse.get_down(MouseButton::Left) {
            let id = { self.0 += 1; self.0 };
            let transform = Transform::from_xyz(250.0, 250.0, 0.0);
            entities.build_entity()
                .with(Icon::new(id), &mut icons)
                .with(sr.clone(), &mut sprite_renders)
                .with(transform, &mut transforms)
                .build();
        }

        // destroy
        if mouse.get_down(MouseButton::Right) && self.0 > 0 {
            let search_id = self.0;
            for (entity, icon) in (&entities, &icons).join() {
                if icon.id == search_id {
                    entities.delete(entity).unwrap();
                    self.0 -= 1;
                    break;
                }
            }
        }
    }
}
```

#### `SystemData`

ある事情により`SystemData`が今までより多くなっています。

- `Entities`
    - 全てのエンティティのストレージ
- `WriteStorage<Icon>`, `WriteStorage<SpriteRender>`, `WriteStorage<Transform>`
    - それぞれ`Icon`,`SpriteRender`,`Transform`コンポーネントへの可変参照ストレージ
- `ReadExpect<SpriteRender>`
    - `Read`と似ていますが、`Default`トレイトが実装されていないリソースへは`ReadExpect`を用います。可変参照として`WriteExpect`を使うこともできます。
- `Read<Mouse>`
    - 自作の`Mouse`構造体をリソースとして呼び出します。

#### create

```rust
if mouse.get_down(MouseButton::Left) {
    let id = { self.0 += 1; self.0 };
    let transform = Transform::from_xyz(250.0, 250.0, 0.0);
    entities.build_entity()
        .with(Icon::new(id), &mut icons)
        .with(sr.clone(), &mut sprite_renders)
        .with(transform, &mut transforms)
        .build();
}
```

`Mouse`構造体の`get_down`メソッドにより、引数のボタンが「今押されたか」を判定できます。これが`true`になるのは押されてから1フレームのみです。もし`InputHandler`から取得しようとしてしまうと、ボタンを離すまでに何フレームも進んでしまい意図しない量のエンティティが生成されてしまいます。

先ほどインポートした`TransformExt`により`Transform::from_xyz`が使えるようになっています。
エンティティの登録方法はいままで行なっていた方法と似ていますが、`Entities`の`build_entity`メソッドを用いた場合は`with()`の引数をひとつ増やし、どのストレージに登録するかを指定しなければなりません。そのために複数の`WriteStorage`を`SystemData`に追加していました。
`sr`というのは、いままでエンティティに付加していた`SpriteRender`コンポーネントです。今回はリソースとして登録しているので（後ほど出てきます）、それをクローンして使い回しています。

#### destroy

```rust
if mouse.get_down(MouseButton::Right) && self.0 > 0 {
    let search_id = self.0;
    for (entity, icon) in (&entities, &icons).join() {
        if icon.id == search_id {
            entities.delete(entity).unwrap();
            self.0 -= 1;
            break;
        }
    }
}
```

`Entities`と`WriteStorage<Icon>`を結びつけて走査していきます。特定のidを持つ`Icon`コンポーネント、を持つエンティティを削除しています。

## `struct ExampleState`

```rust
struct ExampleState;

impl SimpleState for ExampleState {
    fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
        let world = data.world;
        initialise_camera(world, [500.0, 500.0]);
        initialise_mouse(world);
        world.register::<Icon>();

        let sprite_sheet = load_sprite_sheet(
            world,
            "icon.png",
            "spritesheet.ron"
        );
        let sprite_render = SpriteRender {
            sprite_sheet: sprite_sheet,
            sprite_number: 0,
        };
        world.add_resource(sprite_render);
    }

    fn handle_event(
        &mut self,
        _: StateData<'_, GameData<'_, '_>>,
        event: StateEvent,
    ) -> SimpleTrans {
        if let StateEvent::Window(e) = event {
            if is_key_down(&e, VirtualKeyCode::Escape) {
                return Trans::Quit;
            }
        }
        Trans::None
    }
}
```

`on_start`メソッド内部では、`lib.rs`で定義されている`initialise_camera`,`initialise_mouse`関数を呼び出しています。その下では、`load_sprite_sheet`関数を呼び出して、`SpriteSheet`のオブジェクトを取得しています。このオブジェクトを`world.add_resource()`でリソースとして登録することにより、`ReadExpect`から利用することができるようになります。
`handle_event`はいつも通りなので飛ばします。

## `struct MoveSystem`

エンティティの移動と跳ね返りを制御しています。特に目新しいこともしていないので省略します。

## `main.rs`

```rust
fn main() -> amethyst::Result<()> {
    /* body omitted */

    let input_bundle = InputBundle::<String, String>::new();
    let mouse_bundle = MouseBundle::new();

    let game_data = GameDataBuilder::new()
        .with_bundle(render_bundle.with_sprite_sheet_processor())?
        .with_bundle(transform_bundle)?
        .with_bundle(input_bundle)?
        .with_bundle(mouse_bundle)?
        .with(CreateDestroySystem(0), "create-destroy-system", &[])
        .with(MoveSystem, "move-system", &[]);

    Application::new(
        "./examples/04_create_and_destroy/",
        ExampleState,
        game_data
    )?.run();

    Ok(())
}
```

バンドルは、システムを追加する際に行いたい処理をまとめておくような使い方ができます。例えば、`lib.rs`に書かれている`MouseSystem`は`SystemData`に`InputHandler`を用いているので、依存関係に書いておかなければなりません。その場合

```rust
    .with(MouseSystem, "mouse-system", &["input_system"])
```

となります（`"input_system"`という名前はソースコードから探して来ました）。これにより、`InputSystem`が追加されていないとエラーにしてくれますが、いちいち書くのは面倒です。しかし`MouseBundle`の内部でシステムの追加と依存関係をあらかじめ与える処理を書いているので、バンドルの追加だけで済みます。他には、複数のシステムをまとめて追加する処理を書くこともできます。詳しくはサンプルコードを見てください。

あとは今まで通りですね。`cargo run --example 04`で実行しましょう！左クリックでエンティティ(動くアイコン)の追加、右クリックでエンティティを順番に削除します。

### おわりに

お疲れ様でした。一つの回に複数の題材を盛り込んでいるので割とゲーム製作に必要な情報が揃って来ましたね。もうamethyst公式がよく例に出しているピンポンゲームくらいなら作れるでしょう。
もし今回・これまでの回でわからなかったところや疑問に思ったこと、アドバイスなどありましたらぜひコメントしてください！

次はキーボードからの入力について触れていきます。
キーボードからの入力を扱えるようになったら、いよいよ簡単なゲームを作ってみましょう！
それでは。