LoginSignup
2
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-13

<< #3 マウス入力と座標変更  #5 キーボード入力 >>

ご注意!

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

はじめに

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

サンプルコードはこちら。今回はexamples/04_create_and_destroyになります。

main.rs

lib.rsからインポート

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

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構造体を定義して、リソースとして呼び出すことでマウスボタンの状態を取得できるようにしています。詳しくはソースコードを見てください。

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

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

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

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

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

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が使えるようになっています。
エンティティの登録方法はいままで行なっていた方法と似ていますが、Entitiesbuild_entityメソッドを用いた場合はwith()の引数をひとつ増やし、どのストレージに登録するかを指定しなければなりません。そのために複数のWriteStorageSystemDataに追加していました。
srというのは、いままでエンティティに付加していたSpriteRenderコンポーネントです。今回はリソースとして登録しているので(後ほど出てきます)、それをクローンして使い回しています。

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

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

struct ExampleState

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

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に書かれているMouseSystemSystemDataInputHandlerを用いているので、依存関係に書いておかなければなりません。その場合

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

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

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

おわりに

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

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

2
2
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
2
2