LoginSignup
3
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-07

<< #5 キーボード入力

ご注意!

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

はじめに

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

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

spritesheet.ron

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

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

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

main.rs

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));
}

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

struct CharaEntity(Entity);

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

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キーで終了する処理を記述しています。

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でやっていることと同じで、スプライトシートと設定ファイルからSpriteSheet構造体を作成し、SpriteRenderコンポーネントを作成し与えています。
先ほどエンティティについて触れた際に説明が入りましたが、.create_entity()で作成したエンティティを.build()した際、その返り値であるEntity構造体をentity変数に一度束縛し、CharaEntity構造体の初期化に与えています。と同時に.add_resource()CharaEntityを登録しています。
そして次でいよいよ、今まで用意してきたものを利用します。

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関数は、繰り返される数値の拡張を行っています。なんのこっちゃですねw 例を挙げます。
スプライトナンバーをフレームカウントで制御したいとします。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

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

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,...といった感じにそれぞれの数値が引き伸ばされます。

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で実行して、真ん中に巫女服をたなびかせている女の子が現れたら成功です!

終わりに

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

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1