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

<< [#5 キーボード入力](https://qiita.com/9laceef/items/6909e0b5633938667af4)

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

### はじめに

更新が遅くなってしまいすみません！（しゅ、就活が...）
当分更新やめる気はないので、気長に待っていただければと思います...！

以前スプライトについて少しだけ触れましたが、まだアニメーションを表現したことはありませんでした。今回は次の記事の前準備としてアニメーションについて触れていこうと思います。
制作物のコードはGitHubに上げています。[こちら](https://github.com/glaceef/amethyst-test)より利用してみてください。
今回は`examples/06_animation`になります。

## `spritesheet.ron`

2Dアニメーションを表現するためのスプライトという技法では、表示するものを連続で変え、パラパラ漫画のように動いているように見せかける方法をとります。基本的には、連続で表示したい画像を一枚のファイルにまとめて、表示する位置をずらしていくことでアニメーションを表現します。
amethystのスプライトシートの設定ファイルでは、表示する位置を配列にして記述します。

```ron
(
    spritesheet_width: 256,
    spritesheet_height: 256,
    sprites: [
        (
            x: 1,
            y: 1,
            width: 48,
            height: 48,
        ),
        (
            x: 51,
            y: 1,
            width: 48,
            height: 48,
        ),
        ...
    ]
)
```

ronファイルの記述方法は[こちら](https://github.com/ron-rs/ron#rusty-object-notation)。
一部省略していますが、`sprites`フィールドに表示する範囲の(左上x座標, 左上y座標, 横幅, 縦幅)を与えています。例えば配列のインデックスが`0`であれば画像の`(1, 1, 48, 48)`の範囲を、インデックスが`1`であれば`(51, 1, 48, 48)`の範囲を表示します。このインデックスを変化させることによってアニメーションが表現できます

## `main.rs`

```rust
struct CharaEntity(Entity);

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_character(world);
    }

    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 CharaSpriteSystem(usize);

impl<'s> System<'s> for CharaSpriteSystem {
    type SystemData = (
        WriteStorage<'s, SpriteRender>,
        ReadExpect<'s, CharaEntity>
    );

    fn run(&mut self, (mut sprites, player): Self::SystemData) {
        match sprites.get_mut(player.0) {
            Some(sprite) => {
                sprite.sprite_number = num_extend(self.0, 4, 10);
                self.0 += 1;
            }
            _ => {}
        }

        fn num_extend(n: usize, size: usize, repeat: usize) -> usize {
            n % (size * repeat) / repeat
        }
    }
}

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

    let app_root = PathBuf::from(application_root_dir()).join("examples/06_animation/");

    let pipe = Pipeline::build().with_stage(
        Stage::with_backbuffer()
            .clear_target([0.001, 0.0, 0.02, 1.0], 1.0)
            .with_pass(DrawFlat2D::new().with_transparency(
                ColorMask::all(),
                ALPHA,
                Some(DepthMode::LessEqualWrite)
            ))
    );
    let config = DisplayConfig::load(app_root.join("config.ron"));
    let render_bundle = RenderBundle::new(pipe, Some(config));

    let transform_bundle = TransformBundle::new();

    let game_data = GameDataBuilder::new()
        .with_bundle(render_bundle.with_sprite_sheet_processor())?
        .with_bundle(transform_bundle)?
        .with(CharaSpriteSystem(0), "chara_sprite_system", &[]);

    Application::new(app_root, ExampleState, game_data)?.run();

    Ok(())
}

fn initialise_character(world: &mut World) {
    let sprite_sheet = load_sprite_sheet(world, "dot_reimu.png", "spritesheet.ron");
    let sprite_render = SpriteRender {
        sprite_sheet: sprite_sheet,
        sprite_number: 0,
    };

    let scale = 2.0;
    let mut transform = Transform::from_xyz(250.0, 250.0, 0.0);
        transform.set_scale(scale, scale, 1.0);

    let entity = world.create_entity()
        .with(sprite_render)
        .with(transform)
        .build();
    world.add_resource(CharaEntity(entity));
}
```

ここでは単一のエンティティを特別に保持して利用しています。

```rust
struct CharaEntity(Entity);
```

まずエンティティとは、一般的に（`amethyst`クレートの仕様は調べていません）ゲームにおけるオブジェクトを示すIDとコンポーネントを保持しています。あるコンポーネントの集まりに対し「このエンティティが持っているのはどれ？」と問い、対応するコンポーネント1つを取得するという使い方ができます（`CharaSpriteSystem`にて説明します）。
これまでエンティティを生成する際は、`world.create_entity()`で生成したあと`.with()`でコンポーネントを与えていき最後に`.build()`で締めていましたが、実はこの返り値が`Entity`となっています（`initialise_character`関数にて説明します）。

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

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

`on_start`関数内ではカメラの設定とキャラクターの初期化をしています。`handle_event`関数内ではいつものEscキーで終了する処理を記述しています。

```rust
fn initialise_character(world: &mut World) {
    let sprite_sheet = load_sprite_sheet(world, "dot_reimu.png", "spritesheet.ron");
    let sprite_render = SpriteRender {
        sprite_sheet: sprite_sheet,
        sprite_number: 0,
    };

    let scale = 2.0;
    let mut transform = Transform::from_xyz(250.0, 250.0, 0.0);
        transform.set_scale(scale, scale, 1.0);

    let entity = world.create_entity()
        .with(sprite_render)
        .with(transform)
        .build();
    world.add_resource(CharaEntity(entity));
}
```

だいたいの処理は[#4](https://qiita.com/9laceef/items/04e6221ff62fa3fe1b25)でやっていることと同じで、スプライトシートと設定ファイルから`SpriteSheet`構造体を作成し、`SpriteRender`コンポーネントを作成し与えています。
先ほどエンティティについて触れた際に説明が入りましたが、`.create_entity()`で作成したエンティティを`.build()`した際、その返り値である`Entity`構造体を`entity`変数に一度束縛し、`CharaEntity`構造体の初期化に与えています。と同時に`.add_resource()`で`CharaEntity`を登録しています。
そして次でいよいよ、今まで用意してきたものを利用します。

```rust
struct CharaSpriteSystem(usize);

impl<'s> System<'s> for CharaSpriteSystem {
    type SystemData = (
        WriteStorage<'s, SpriteRender>,
        ReadExpect<'s, CharaEntity>
    );

    fn run(&mut self, (mut sprites, player): Self::SystemData) {
        match sprites.get_mut(player.0) {
            Some(sprite) => {
                sprite.sprite_number = num_extend(self.0, 4, 10);
                self.0 += 1;
            }
            _ => {}
        }

        fn num_extend(n: usize, size: usize, repeat: usize) -> usize {
            n % (size * repeat) / repeat
        }
    }
}
```

このシステムでは、キャラクターのアニメーションを表現するために、スプライトシートの表示位置配列のインデックを変更しています。
最初に`spritesheet.ron`で説明した表示位置のデータをまとめた配列がありましたね。`SpriteRender`コンポーネントはこのインデックスを保持しており、これを操作します。

このシステムにおけるシステムデータは`WriteStorage<SpriteRender>`と`ReadExpect<CharaEntity>`です。`WriteStorage`はコンポーネントの集まりの可変参照ですね（コンポーネントの可変参照の集まり？）。`ReadExpect`については#4で解説しています。
`~~Storage<T>`は`get()`もしくは`get_mut()`にエンティティを与えると、対応するコンポーネント（そのエンティティに付加されたコンポーネント）が取得できます。別の方法で、例えばキャラクターを識別するために`Player`というユニット構造体を定義しエンティティに与え、`join()`などで`Player`コンポーネントも`SpriteRender`コンポーネントも持つエンティティのみを選別する方法がありますが、それよりも手間が少なく直感的であるという利点があります。逆に`SpriteRender`をもつ複数のエンティティに同時に適用させたい場合などはこの方法は不向きでしょう。単一のエンティティを特別に利用したい場合に用いるべきです。

`num_extend`関数は、繰り返される数値の拡張を行っています。なんのこっちゃですねｗ 例を挙げます。
スプライトナンバーをフレームカウントで制御したいとします。`frame_count % 4`とすると、

```
  frame_count: 0, 1, 2, 3, 4, 5, 6, 7, 8, ...
sprite_number: 0, 1, 2, 3, 0, 1, 2, 3, 0, ... // frame_count % 4
```

といった対応のしかたになります。しかしこれだと変化が速すぎるんですね。
一度プログラムを書き換えて、

```rust
sprite.sprite_number = self.0 % 4
```

で実行してみてください。言っている意味が分かると思います。

`num_extend`関数の引数は、`(数値, 繰り返すインデックスサイズ, 引き伸ばし数)`になっています。1番目の引数は元の数値です（フレームカウントなど）。2番目の引数に`4`を与えると、`0~3`を繰り返します（%4と同じ）。3番目の引数は各数値の繰り返し数です。`3`を与えると、`0,0,0,1,1,1,2,2,2,3,3,3,0,0,0,...`といった感じにそれぞれの数値が引き伸ばされます。

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

    let app_root = PathBuf::from(application_root_dir()).join("examples/06_animation/");

    let pipe = Pipeline::build().with_stage(
        Stage::with_backbuffer()
            .clear_target([0.001, 0.0, 0.02, 1.0], 1.0)
            .with_pass(DrawFlat2D::new().with_transparency(
                ColorMask::all(),
                ALPHA,
                Some(DepthMode::LessEqualWrite)
            ))
    );
    let config = DisplayConfig::load(app_root.join("config.ron"));
    let render_bundle = RenderBundle::new(pipe, Some(config));

    let transform_bundle = TransformBundle::new();

    let game_data = GameDataBuilder::new()
        .with_bundle(render_bundle.with_sprite_sheet_processor())?
        .with_bundle(transform_bundle)?
        .with(CharaSpriteSystem(0), "chara_sprite_system", &[]);

    Application::new(app_root, ExampleState, game_data)?.run();

    Ok(())
}
```

ふう、長かったですね。最後は`main`関数ですが、特に目新しいこともありません。いつも通りです。

あ、一つ新しいものとして、`application_root_dir()`を用いています。公式exampesでは基本的に用いられているのですが、バージョンが新しくなったため現在返り値が`PathBuf`ではなく`String`になっています。

お疲れさまでした！久しぶりなので説明が不安ですが、わからないことありましたらコメントなどお願いします。
`cargo run --example 06`で実行して、真ん中に巫女服をたなびかせている女の子が現れたら成功です！

## 終わりに

今回のプログラムを製作するにあたり、以前東方弾幕風の素材にてお世話になりました[弾幕工房さん](http://danmakunoc.web.fc2.com/index/player.html)の博麗霊夢の素材を利用させていただきました。readme等一通り目を通しました上で素材元明記しての利用をさせていただきましたが、もし使用停止などありましたらすぐにコメントしていただけると幸いです。

次回、いよいよこれまでの知識を用いてミニゲームを作ってみたいと思います！
お楽しみに～