<< [#3 マウス入力と座標変更](https://qiita.com/9laceef/items/5b39fcf5e6fd6340ec78) [#5 キーボード入力](https://qiita.com/9laceef/items/6909e0b5633938667af4) >>
ご注意!
この記事の内容は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
が使えるようになっています。
エンティティの登録方法はいままで行なっていた方法と似ていますが、Entities
のbuild_entity
メソッドを用いた場合はwith()
の引数をひとつ増やし、どのストレージに登録するかを指定しなければなりません。そのために複数のWriteStorage
をSystemData
に追加していました。
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;
}
}
}
Entities
とWriteStorage<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
に書かれているMouseSystem
はSystemData
にInputHandler
を用いているので、依存関係に書いておかなければなりません。その場合
.with(MouseSystem, "mouse-system", &["input_system"])
となります("input_system"
という名前はソースコードから探して来ました)。これにより、InputSystem
が追加されていないとエラーにしてくれますが、いちいち書くのは面倒です。しかしMouseBundle
の内部でシステムの追加と依存関係をあらかじめ与える処理を書いているので、バンドルの追加だけで済みます。他には、複数のシステムをまとめて追加する処理を書くこともできます。詳しくはサンプルコードを見てください。
あとは今まで通りですね。cargo run --example 04
で実行しましょう!左クリックでエンティティ(動くアイコン)の追加、右クリックでエンティティを順番に削除します。
おわりに
お疲れ様でした。一つの回に複数の題材を盛り込んでいるので割とゲーム製作に必要な情報が揃って来ましたね。もうamethyst公式がよく例に出しているピンポンゲームくらいなら作れるでしょう。
もし今回・これまでの回でわからなかったところや疑問に思ったこと、アドバイスなどありましたらぜひコメントしてください!
次はキーボードからの入力について触れていきます。
キーボードからの入力を扱えるようになったら、いよいよ簡単なゲームを作ってみましょう!
それでは。