<< #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等一通り目を通しました上で素材元明記しての利用をさせていただきましたが、もし使用停止などありましたらすぐにコメントしていただけると幸いです。
次回、いよいよこれまでの知識を用いてミニゲームを作ってみたいと思います!
お楽しみに~