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

<< [System編](https://qiita.com/9laceef/items/b1e9496717e0a60e9b7e)　　[#4 エンティティの作成・削除](https://qiita.com/9laceef/items/04e6221ff62fa3fe1b25) >>

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

### はじめに

今回は、マウスからの入力と`Transform`コンポーネントを扱ってみます。クリックした位置に画像を移動させるプログラムを作ってみます。

[#1](https://qiita.com/9laceef/items/380ac0944199de7aad8e)でウィンドウの設定ファイルについて紹介しました。今回は画像用の設定ファイルについても扱っていきます。

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

## `spritesheet.ron`

画像ファイルの1枚ごとに設定ファイルを用意します。

```ron
(
    spritesheet_width: 50,
    spritesheet_height: 50,
    sprites: [
        (
            x: 0,
            y: 0,
            width: 50,
            height: 50,
        ),
    ],
)
```

`spritesheet_width`, `spritesheet_height`に画像ファイルのサイズを指定します。`sprites`には画像サイズ（左上のx座標、y座標、横幅、高さ）の配列を与えます。

## `main.rs`

\#2では最後にコード全体を紹介していましたが、見やすさのためにこれからは最初に載せようと思います。ただし、インポートや`initialise_camera`関数など使いまわすものに関しては省略させていただきます。前回までの章、もしくは[サンプルコード](https://github.com/glaceef/amethyst-test)を見てください。
基本的に長いので最初は流し読みで大丈夫です。

```rust
struct Icon;

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);
        world.register::<Icon>();
        initialise_icon(world);
    }

    fn handle_event(
        &mut self,
        _: StateData<'_, GameData<'_, '_>>,
        event: StateEvent,
    ) -> SimpleTrans {
        /* body omitted */
    }
}

struct MoveSystem;

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

    fn run(&mut self, (icons, mut transforms, input): Self::SystemData) {
        if input.button_is_down(Button::Mouse(MouseButton::Left)) {
            if let Some((x, y)) = input.mouse_position() {
                for (_icon, transform) in (&icons, &mut transforms).join() {
                    transform.set_xyz(x as f32, 500.0 - y as f32, 0.0);
                }
            }
        }
    }
}

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/03_move_to_mouse/config.ron");
    let render_bundle = RenderBundle::new(pipe, Some(config));

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

    let transform_bundle = TransformBundle::new();

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

    Application::new(
        "./examples/03_move_to_mouse/",
        ExampleState,
        game_data
    )?.run();

    Ok(())
}

fn initialise_camera(world: &mut World) {
    /* body omitted */
}

fn initialise_icon(world: &mut World) {
    let sprite_sheet_handle = {
        let loader = world.read_resource::<Loader>();
        let texture_handle = {
            let texture_storage = world.read_resource::<AssetStorage<Texture>>();
            loader.load(
                "icon.png",
                PngFormat,
                TextureMetadata::srgb_scale(),
                (),
                &texture_storage,
            )
        };

        let sprite_sheet_store = world.read_resource::<AssetStorage<SpriteSheet>>();
        loader.load(
            "spritesheet.ron",
            SpriteSheetFormat,
            texture_handle,
            (),
            &sprite_sheet_store,
        )
    };

    let sprite_render = SpriteRender {
        sprite_sheet: sprite_sheet_handle,
        sprite_number: 0,
    };

    let mut transform = Transform::default();
        transform.set_xyz(250.0, 250.0, 0.0);
    world.create_entity()
        .with(Icon)
        .with(sprite_render)
        .with(transform)
        .build();
}
```

## `struct Icon`

今回は`Icon`構造体を移動させるオブジェクトとします。

```rust
struct Icon;

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

[System編](https://qiita.com/9laceef/items/b1e9496717e0a60e9b7e)で解説した通り、`Component`トレイトを実装させます。これによりエンティティに付加させることができるようになります。

## `initialise_icon`

```rust
fn initialise_icon(world: &mut World) {
    let sprite_sheet_handle = {
        let loader = world.read_resource::<Loader>();
        let texture_handle = {
            let texture_storage = world.read_resource::<AssetStorage<Texture>>();
            loader.load(
                "icon.png",
                PngFormat,
                TextureMetadata::srgb_scale(),
                (),
                &texture_storage,
            )
        };

        let sprite_sheet_store = world.read_resource::<AssetStorage<SpriteSheet>>();
        loader.load(
            "spritesheet.ron",
            SpriteSheetFormat,
            texture_handle,
            (),
            &sprite_sheet_store,
        )
    };

    let sprite_render = SpriteRender {
        sprite_sheet: sprite_sheet_handle,
        sprite_number: 0,
    };

    let mut transform = Transform::default();
        transform.set_xyz(250.0, 250.0, 0.0);
    world.create_entity()
        .with(Icon)
        .with(sprite_render)
        .with(transform)
        .build();
}
```

`Loader`に関しては以前触れましたね。いろいろなアセットを呼び出すことができるこの`load`メソッドですが、今回はスプライトシートを呼び出しています。
余裕が出てくるまではコピペで良いと思います。`"icon.png"`が画像のパス、`"spritesheet.ron"`がスプライトシートの設定ファイルになり、`sprite_number`がspritesheet.ronで設定した描画位置配列のインデックスになります。また今回はアニメーションはせずに1枚全体を使います（スプライトシートを使ったアニメーションをしてみたいと思った方は、`examples/11_animation`(名前変わるかもしれません)などを参考に頑張ってみてください。いつか記事にするとは思いますが）。

余談ですが、スプライトというパラパラ漫画のようにコマ送りでアニメーションを表現する技法があり、それを1枚にまとめて位置だけをずらして描画する方法をとるため、そのまとめた画像（シート）をスプライトシートというそうです。

## `MoveSystem`

このシステムの中で、マウスボタンが押されている間の座標の取得、`Icon`の座標(`Transform`)の変更を行っています。

### `Read`

System編では`Read`と`Write`の説明は省略していました。これは型ごとに一つだけリソースを追加することができます。ストレージにエネミーオブジェクトを複数追加するような使い方ではなく、「時間」や「入力」といったリソースに対して使われます。`~~Storage`と同じように、`Read`は不変参照を、`Write`は可変参照を呼び出します。
このプログラムでは`InputHandler`という、マウスやキーボードからの入力を読むためのリソースを呼び出しています。これは後ほど出てくる`InputBundle`を必要とします。

```rust
struct MoveSystem;

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

    fn run(&mut self, (icons, mut transforms, input): Self::SystemData) {
        if input.button_is_down(Button::Mouse(MouseButton::Left)) {
            if let Some((x, y)) = input.mouse_position() {
                for (_icon, transform) in (&icons, &mut transforms).join() {
                    transform.set_xyz(x as f32, 500.0 - y as f32, 0.0);
                }
            }
        }
    }
}
```

`SystemData`は、`Icon`コンポーネントへの不変参照、`Transform`コンポーネントへの可変参照、そしてマウス入力を読み込むための`InputHandler`への不変参照の３つを割り当てます。
`run`メソッド内にて、`Icon`コンポーネントと`Transform`コンポーネントをもつエンティティにのみ作用させたいので、`join()`で選択します。今回は一つしか作っていませんのでfor文は1回でおわりますね。
`input.mouse_position()`と`transform.set_xyz()`についてなのですが、y座標のみおかしなことになっています。というのも、`Tranform`コンポーネントにとってウィンドウの**下**がy=0になりますが、マウス座標にとってはウィンドウ**上**がy=0なので、ウィンドウの縦サイズから引いて逆転させないと画面真ん中で鏡のような挙動になってしまいます。

## `main()`

ここはこれといった変化はありません。`InputHandler`を扱うために`InputBundle`を用いていますね。

```rust
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/03_move_to_mouse/config.ron");
    let render_bundle = RenderBundle::new(pipe, Some(config));

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

    let transform_bundle = TransformBundle::new();

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

    Application::new(
        "./examples/03_move_to_mouse/",
        ExampleState,
        game_data
    )?.run();

    Ok(())
}
```

上から7行目あたりで行っているpng画像の透過処理は忘れないようにしましょう。

お疲れさまでした！`cargo run --example 03`で実行されたら画面の適当なところをクリックしてみたり押しながら動かしたりしてみましょう。また別の画像を用意してspritesheet.ronの使い方に慣れてみてもいいですね。

###　おわりに

以前デバッグのためにウィンドウタイトルを動的に変更できないかと試行錯誤していて結局できなかったのですが、ウィンドウサイズもおそらく取得できないかもしれません。そうなるとマウス座標から作用させるのにハードコーディングさせる必要が出てくるというのが厄介です...。けれど`Read`でいろいろリソースを取得できるので、これで何か得られるかもしれませんね？

次回は、何らかのアクションにより後からエンティティを追加してみようと思います。
ではでは。