##0. はじめに
この記事はRustで書かれたゲームエンジンAmethystの入門記事です。もう既にゲーム開発したことがある方には新しい選択肢を、まだゲーム開発したことはない方にはゲーム開発のきっかけを提供できるような記事にしていこうと思います。
この記事は連載形式をとっていこうと考えています。Amethystは日本語のレファレンスが存在しなく、Amethystの学習参入ハードルが高いと思うのですが、これを見て勉強してみようかなと思える連載にしようと思います。
今回の記事は初回なのでAmethystの概要を紹介をして、次回からより詳細に踏み込んでいこうと思います。
##1. Amethystって?
Rustで書かれたオープンソースのゲームエンジンです。
###1-1. ゲームエンジン
ゲームエンジンとはゲームを開発する上で便利なツールをまとめた開発環境のことです。Web開発でいうところのフレームワークみたいなものです。便利なツールというのはページ遷移を管理するマネージャーであったり、オブジェクトを描画してくれるレンダラーなどのことで、これらを自分で実装すると汎用的な作業であるのに毎回手間がかかるデメリットがあるので、それを解決するためにゲームエンジンが存在します。
###1-2. プログラミング言語Rust
RustはMozillaが支援しているシステムプログラミング言語で、C言語やC++の代わりとなる言語を目指しているそうです。近年Rustを使って、新しいソフトウェアが開発されたり既存ソフトウェアが書き換えられたりしています。例えばFacebookが開発していた仮想通貨LibraはRustで書かれています。MicrosoftはC言語やC++で書かれていたWindowsのコンポーネントをRustで書き換える実験を行っているそうです。
そんなRustの最大の特徴はメモリ管理において非常に安全で効率的な所です。
CやC++においてメモリ管理はプログラマが管理していたのに対して、Rustではコンパイラが安全でない使い方をしている場合エラーを出してくれます。これはソフトウェアが大きくなればなるほどバグを見つけるのが大変なので非常に効果的です。また、Rustは使わなくなった変数はすぐに破棄する性質があるのでメモリ効率が非常に良いです。ゲームソフトはソースコードが膨大でバグの発見に大きなコストがかかり、また実行時にメモリの節約が求められるので、Rustはゲーム開発に最適な言語と言えます。
Rustに興味を持った方は以下をどうぞ。
・ Rustのメモリ管理に関しては次の記事がよくまとめられています。
Rustのメモリ管理って面白い
・ Rustは仕様書が簡潔にまとめられていて、日本語版もあるので困った時は安心です。リンクはこちら。
###1-3. Amethyst
AmethystはRustで書かれたゲームエンジンです。
Rust製のゲームエンジンはいくつかあって3Dゲームを作るならAmethystかPiston、2Dゲームを作るなら前記の2つに加えてggezというエンジンが多く使われています。
Amethystの最大の特徴はEntityComponentSystem(ECS)という設計方針で、これについては次の項で詳しく説明します。
##2. 設計思想
2-1. Entity Component System(ECS)
AmethystはECSをもとに設計されていて、ECSとはEntity、Component、Systemそれぞれの頭文字をとっています。Entityとは英語で実在、Componentは英語で部品を意味します。Systemはそのままシステムです。それらの関係のイメージを掴みやすくするために具体例で考えてみます。例えば手元にスマートフォンが何十台かあり、それぞれが様々な属性をもっています。あるスマートフォンはApple製でサイズは5インチで色は黒かもしれません。別のスマートフォンはSamsung製でサイズは5インチで色は白色かもしれません。これを踏まえて各スマートフォンが属性(部品)を数種類持っていると考える設計をECSと言います。つまり、スマートフォンをEntityとし、メーカーComponent、サイズComponent、カラーComponentを所有しているという考え方がECSの設計方針です。各Componentはそれぞれ専用のStorageに保存されていて、またEntityのIDが共有されているおかげで同一Entityの別のComponent同士をJoinすることが可能になります。なのでEntityがあればそのEntityを保持する全てのComponentにアクセス可能ですし、ComponentからEntityを介して別のComponentにアクセスすることも可能になります。
各ComponentはグローバルなStorageに保存されているのでゲーム中どこからでも呼び出すことが可能です。この性質を利用してSystemは動いています。Systemは同時に何個も動かすことができ、それぞれが複数のComponentを読み込んで処理を実行し、Componentを更新することでゲームの状態を変化させます。
以下の図はECSのイメージをつかむために公式サイトから引用したものです。Entityをボトルとし、座標や形状や所有者をそれぞれComponentとして用意しています。図をみると分かると思うですがEntityは実はただのIDで各ComponentがEntityを持っているという設計になっています。
https://book.amethyst.rs/stable/concepts/entity_and_component.html
2-2. ECSのメリット、デメリット
ECSの最大のメリットはマルチスレッドが安全に実行できるという点です。Systemを複数実行する時、書き込みをするComponentを共有していない場合はそのSystem同士を並列に実行してもメモリロッキングが発生しないのでマルチスレッドを安全に実行できます。逆に書き込みをするComponentを共有している場合は同時に書き込みをする危険性があるので並列に実行することはしません。以上によってECSはマルチスレッドを安全に実行しています。
ECSのデメリットとしては複数のSystemの間でデータのやりとりをするのが少し面倒という点があります。System間の通信には、グローバルに保存できる監視役のような存在が必要でSystemとは別で実装します。これはAmethystではResourceと呼ばれていて、System間のやりとりを行います。
##3. 実際に何か作ってみる
以上がAmethystの概要で次に実際にAmethystを使って何かを作ってみようと思います。
###3-1. 環境構築
まずこちらにしたがってRustをインストールしましょう。ここではRustupというバージョン管理ツールをインストールします。(同時に最新のRustもインストールしてくれます)
curl https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env
Windowsを使っている人はhttps://www.rust-lang.org/tools/install
にアクセスして、指示にしたがってインストールをしてください。
次にAmethystをインストールしましょう。
cargo install amethyst_tools
このコマンドでAmethystのツールを全て取得できます。
ゲームのフォルダを作成するのは以下をターミナルに打ち込むだけです。簡単ですね。
amethyst new <game-name>
ゲームを実行する時は以下のコマンドを打ち込みましょうGPUドライバーはMacであればMetal、WindowsかLinuxであればVulkanを使用しましょう。
cargo run --features metal
cargo run --features vulkan
###3-2. 岩が障害物をぴょんぴょん飛びこえるゲーム
文字通り岩が障害物を飛び越えるゲーム、Super Rockというゲームを作ってみます(Super Marioみたいな2Dゲームを目標にします)。この連載ではこのSuper Rockというゲームをどんどん改造していく形でAmethystの紹介をしていく予定です。今回は岩の操作と、動く障害物を実装しました。次回はおそらく当たり判定を実装します。
まず今回の目標はこんな感じです。障害物が自動で動く実装にしています。(Flappy Birdみたいな感じですね)
おおまかな方針としては、岩や障害物などのEntityを追加してSystem側でComponentを更新して色々な動きをさせて行きます。
####3-2-1. main関数
3-1項のamethystのコマンドを実行すると自動でいくつかのファイルとフォルダができていると思います。
以下に説明を書きます。
- srcフォルダ: デフォルトではmain.rsのみ入っていて、ソースコードはこのフォルダに作っていきます。
- configフォルダ: 設定ファイルが入っていて、デフォルトではディスプレイ設定を記述したdisplay.ronというamethyst独自のron形式のファイルが入っています。
- targetフォルダ: ライブラリやビルド後に生成されるバイナリなどが入っています。
- Cargo.toml: 依存関係やOS依存のGPUのAPIなどを記述します。
- Readme.md, .gitignore: よくあるやつです。
なのでまずmain.rsのmain関数を見ます。
fn main() -> amethyst::Result<()> {
amethyst::start_logger(Default::default());
let app_root = application_root_dir()?;
// スクリーンサイズなどの設定ファイルのパス
let config_dir = app_root.join("config");
let display_config_path = config_dir.join("display.ron");
// ゲームのデータを作成し、システムや設定を追加していく
let game_data = GameDataBuilder::default()
.with_bundle( // レンダリングのシステム
RenderingBundle::<DefaultBackend>::new()
.with_plugin(
RenderToWindow::from_config_path(display_config_path)
.with_clear([0.34, 0.36, 0.52, 1.0]),
)
.with_plugin(RenderFlat2D::default()), //フラットシェーディング
)?
.with_bundle(TransformBundle::new())?; //entityの移動、変形のシステム
// アセットのパス、初期State、ゲームデータによってゲームを作成
let mut game = Application::new("/", (), game_data)?;
// ゲームの実行
game.run();
Ok(())
}
こんな感じのが書いてあります。Amethystのゲームは、Systemを複数登録したgame_dataを作成しそれを元に作成します。gameを作成する時にアセットのパスと初期Stateも同時に登録する必要があるのですが、この項ではassetsのパスを追加します。まずsrcやconfigが存在するプロジェクトフォルダにassetsフォルダを追加してください。
super_rock
|_src
|_config
|_assets *\new/*
その後、main関数内にassetsのパスを作成し、それをgameに追加します。
/**/
let display_config_path = config_dir.join("display.ron");
// アセットのパス
let assets_dir = app_root.join("assets");
/**/
// アセットのパスと初期Stateとゲームデータによってゲームを作成
let mut game = Application::new(assets_dir, (), game_data)?;
全てのコードを最後の方に載せているのですが、順を追って実装していきたい方もいると思うので、今回使うcrateを全て最初にインポートしておきます。あとパラメータ関連も定義しておきます。
use amethyst::{
assets::{AssetStorage, Handle, Loader},
core::{timing::Time, transform::TransformBundle, Transform},
ecs::prelude::*,
ecs::System,
input::{InputBundle, InputHandler, StringBindings, VirtualKeyCode},
prelude::*,
renderer::{
camera::Camera,
formats::texture::ImageFormat,
plugins::{RenderFlat2D, RenderToWindow},
sprite::{SpriteRender, SpriteSheet, SpriteSheetFormat},
types::DefaultBackend,
RenderingBundle, Texture,
},
utils::application_root_dir,
};
/// パラメータ
const SCREEN_WIDTH: f32 = 500.;
const SCREEN_HEIGHT: f32 = 500.;
const OBSTACLE_WIDTH: f32 = 303.;
const OBSTACLE_HEIGHT: f32 = 302.;
const ROCK_HEIGHT: f32 = 52.;
const GRAVITY: f32 = -0.5;
####3-2-2. Stateの用意
Amethystは画面の遷移をStateを用いて行います。Stateというのはゲームでいうとメイン画面のMainState、ローディング画面のLoadingState、ゲーム画面のGamePlayStateなど、ゲームの状態を表しています。Stateはスタックに保存され、Stateの遷移はPopとPushで表現されます。今回はまずゲームプレイのState、PlayStateを作成します。
main.rsに以下を追加します。
/// ゲーム画面のState
struct PlayState;
これだけではStateにならないので以下のSimpleStateというAmethystに実装されているTraitを使用します。
/// SimpleStateというTraitを使用します
impl SimpleState for PlayState {
/// PlayStateへ遷移した瞬間に実行されます
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
}
/// PlayStateがPopされるときに実行されます
fn on_stop(&mut self, data: StateData<'_, GameData<'_, '_>>) {
data.world.delete_all();
}
}
SimpleStateには何個か関数が存在して、今回はあるStateへの遷移時に呼び出されるon_startと、StateがPopされた時に呼び出されるon_stopを使用します。
ここでWorldについての説明なのですが、Worldとはゲーム内でグローバルに保持されるデータのことで、システム間のやりとりや、State間でのやりとり、システムとState間でのやりとりなど、横断的な処理をしたい時に使用されます。Entityを追加するのにもこのWorldが必要になってきます。
引数のdataはそのWorldの情報をもっており、data.worldでWorldにアクセスすることができます。
次に、このStateへゲームの起動時に行きたいのでmain関数を以下のように変更します。
//main関数内
let mut game = Application::new(assets_dir, PlayState, game_data)?;
####3-2-3. EntityとComponentの追加
Worldに実際にEntityを追加していきます。今回追加するのはカメラ、岩、障害物の3つです。これらのEntityはPlayStateが始まった時に追加したいのでon_startを以下のように書き換えます。各関数の引数で共通しているdata.worldは先述したWorldです。sprite_sheet_handleに関しては後述します。
impl SimpleState for PlayState {
/// PlayStateへ遷移した瞬間に実行されます
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
// EntityをWorldに追加します
let sprite_sheet_handle = load_sprite_sheet(data.world);
set_camera(data.world); //カメラをWorldに追加
set_rock(data.world, sprite_sheet_handle.clone()); //岩をWorldに追加
set_obstacle(data.world, sprite_sheet_handle); //障害物をWorldに追加
}
/// PlayStateがPopされるときに実行されます
fn on_stop(&mut self, data: StateData<'_, GameData<'_, '_>>) {
data.world.delete_all();
}
}
EntityをWorldに追加する関数を1つづつ作っていきます。それぞれの関数はファイル内でグローバルな関数です。
まずカメラをセットするset_cameraを作ります。カメラに関しては位置を調整するTransform、カメラの情報を保持するCameraの2つのComponentを使用してEntityを作成します。CameraはAmethystに実装されているのでインポートするだけで大丈夫です。
/// カメラEntityをWorldに追加します
pub fn set_camera(world: &mut World) {
let mut camera_transform = Transform::default(); // カメラの位置を調整するためのComponent
camera_transform.set_translation_xyz(SCREEN_WIDTH / 2., SCREEN_HEIGHT / 2., 1.0); //スクリーンの中心に設置、高さは1.0のところ
world
.create_entity()
.with(camera_transform)
.with(Camera::standard_2d(SCREEN_WIDTH, SCREEN_HEIGHT))
.build();
}
次に岩をセットするset_rockを作ります。この関数に使用するComponentは3つあります。1つ目は岩の座標変更を担当してくれるTransform。2つ目は岩の情報を保持するRock。3つ目は岩のテクスチャを描画するSpriteRenderです。3つ目のComponentについては後述します。ここでRockというComponentについてですが、これは自分で作る必要があります。Componentの作りかたとしては、ComponentというTraitを使用します。その時にComponentを保存するストレージを選択できるのですが、今回は一番スタンダードなDenseVecStorageを使用します。以下にコードを載せました。
/// 岩EntityをWorldに追加します
pub fn set_rock(world: &mut World, sprite_sheet_handle: Handle<SpriteSheet>) {
let mut rock_transform = Transform::default();
rock_transform.set_translation_xyz(SCREEN_WIDTH / 4., 0., 0.);
let rock_sprite_render = SpriteRender {
sprite_sheet: sprite_sheet_handle,
sprite_number: 0, //SpriteSheet中の画像の1つ目
};
world
.create_entity()
.with(rock_transform)
.with(Rock::new())
.with(rock_sprite_render)
.build();
}
/// 岩の情報を保持するComponent
pub struct Rock {
y: f32, // 今回の岩は上下するだけなのでy座標のみ
velocity: f32, // 現在の速度を保持
}
impl Rock {
pub fn new() -> Rock {
Rock {
y: 100.,
velocity: 0.,
}
}
}
/// ComponentというTraitを用いてRockをComponentにする
impl Component for Rock {
type Storage = DenseVecStorage<Self>;
}
次は障害物をセットするset_obstacleを作ります。これも岩と同様に作ります。
/// 障害物EntityをWorldに追加します
pub fn set_obstacle(world: &mut World, sprite_sheet_handle: Handle<SpriteSheet>) {
let mut obstacle_transform = Transform::default();
obstacle_transform.set_translation_xyz(SCREEN_HEIGHT - 10., OBSTACLE_HEIGHT / 2. - 30., 0.);
let obstacle_sprite_render = SpriteRender {
sprite_sheet: sprite_sheet_handle,
sprite_number: 1, // SpriteSheet中の画像の2つ目
};
world
.create_entity()
.with(obstacle_transform)
.with(Obstacle::new())
.with(obstacle_sprite_render)
.build();
}
/// 障害物の情報を保持するComponent
pub struct Obstacle {
x: f32, // 障害物は右から左にいくだけなのでx座標のみ
}
impl Obstacle {
pub fn new() -> Obstacle {
Obstacle {
x: SCREEN_HEIGHT - 10.,
}
}
pub fn set_x(&mut self, new_x: f32) {
self.x = new_x;
}
}
/// ComponentというTraitを用いてObstacleをComponentにする
impl Component for Obstacle {
type Storage = DenseVecStorage<Self>;
}
最後にsprite_sheet_handleを作ります。SpriteSheetというのは2Dゲームにでてくる様々な画像を1つの画像にまとめることでそれぞれの画像のフォルダを作る手間を省いたものです。以下の画像が今回使うSpriteSheetです。まずこれをダウンロードしてください。そしてassetsフォルダーの下にtextureフォルダを作成し、そこに移してください。名前はspritesheet.pngでお願いします。
次にspritesheet.ronというファイルを以下に載せたのでこれも作成し、texture以下に置いてください。
このronというのはAmethystがオリジナルに作成した設定ファイルで、画像全体の高さと幅、各画像の座標や高さや幅を記述しています。
(
texture_width: 358,
texture_height: 302,
sprites: [
(
x: 0,
y: 0,
width: 55,
height: 52,
),
(
x: 55,
y: 0,
width: 303,
height: 302,
)
]
)
assets
|_texture
|_spritesheet.png
|_spritesheet.ron
以上を追加したらSpriteSheetのローダーを作成します。ここではLoaderというResource(2−2項参照)を用いてテクスチャを読み出し、それと設定ファイルspritesheet.ronによってSpriteSheetのハンドラーを作成しています。
/// 2Dゲームに必要な画像をまとめたファイルSpriteSheetをロード
pub fn load_sprite_sheet(world: &World) -> Handle<SpriteSheet> {
let loader = world.read_resource::<Loader>(); // LoaderはResource
let texture_handle = {
let texture_storage = world.read_resource::<AssetStorage<Texture>>();
loader.load(
"texture/spritesheet.png",
ImageFormat::default(),
(),
&texture_storage,
)
};
let sprite_sheet_store = world.read_resource::<AssetStorage<SpriteSheet>>();
loader.load(
"texture/spritesheet.ron",
SpriteSheetFormat(texture_handle),
(),
&sprite_sheet_store,
)
}
####3-2-4. Systemの実装
これまでにEntityとそれに付随するComponentを追加したので、今からそれらを操作するSystemを作ります。
まずPlaySystemという構造体を作成します。
/// ゲームを実行するシステム
pub struct PlaySystem;
次にSystemというTraitを用いてPlaySystemをSystemにします。以下のrun関数はSystemの実行関数でこの関数にこのSystemで使用するComponentのリストを渡します。そのリストはSystemDataというタイプを用いることで作成することができます。その時にそのComponentに対して行いたい操作についても明記します。例えばComponentを読み込みたいだけだったらReadStorage、書き込みをしたいのであればWriteStorageを使用します。この操作は何種類かあって、Read、ReadStorage、ReadExpect、Write、WriteStorage、WriteExpectなどなど。Storage系はComponentに、Expect系はResourceに関連しています。ReadとWriteの単体のものは、対象のComponentが存在しない時に自動で生成する機能があります。
今回は岩と障害物の動かすのと、ユーザーの入力を受け付けるSystemなのでそれに必要なComponenttを使用します。
/// システムを作るときはSystemのTraitを用います
impl<'a> System<'a> for PlaySystem {
// このシステムで使用するComponentの種類
type SystemData = (
WriteStorage<'a, Transform>, // TransformはEntityの座標やサイズをそうさするComponentへの書き込み
WriteStorage<'a, Rock>, // 岩の情報を持ったComponentへの書き込み
WriteStorage<'a, Obstacle>, // 障害物の情報を持ったComponentへの書き込み
Read<'a, InputHandler<StringBindings>>, // ユーザーからの入力に関するResourceを読み込み
Read<'a, Time>, // 時間Resourceの読み込み
);
/// システムの実行関数
fn run(&mut self, (mut transforms, mut rocks, mut obstacles, input, time): Self::SystemData) {
}
次に重力にしたがって落ちる岩、右から左に移動する障害物、ユーザーの入力を受け取って上に加速する岩をSystem上に実装します。run関数に以下を追加します。同一のEntityを共有しているComponent同士を得るにはjoin()という関数を用います。これによって岩と障害物、別々に操作します。
/// システムの実行関数
fn run(&mut self, (mut transforms, mut rocks, mut obstacles, input, time): Self::SystemData) {
// joinによってEntityを共有しているComponentの集合を得ることが可能
for (transform, rock) in (&mut transforms, &mut rocks).join() {
// 前のフレームからの時間経過を取得
let dt = time.delta_real_seconds() * 70.;
// Enterキーの入力を検知
if input.key_is_down(VirtualKeyCode::Return) {
// 上向きへの速度を与えることでジャンプ
rock.set_velocity(7.);
}
// 基本的には下向きへの加速
let mut new_velocity = rock.velocity + GRAVITY * dt;
let mut new_y = rock.y + new_velocity * dt;
// 地面についたら速度が0になる
if new_y <= ROCK_HEIGHT / 2. {
new_velocity = 0.;
new_y = ROCK_HEIGHT / 2.;
}
// 実際に岩エンティティの座標を変更し、岩の情報も更新
transform.set_translation_y(new_y);
rock.set_y(new_y);
rock.set_velocity(new_velocity);
}
// 同様に障害物のComponentを取得
for (transform, obstacle) in (&mut transforms, &mut obstacles).join() {
// 左に進みます
let dt = time.delta_real_seconds() * 70.;
let mut new_x = obstacle.x - 5. * dt;
// 左端についたら右端へ移動させます
if new_x <= -OBSTACLE_WIDTH / 2. {
new_x = SCREEN_WIDTH;
}
obstacle.set_x(new_x);
transform.set_translation_x(new_x);
}
}
PlaySystemで使用する関数をRockとObstacleに追加します。
impl Rock {
pub fn new() -> Rock {
Rock {
y: 100.,
velocity: 0.,
}
}
pub fn set_velocity(&mut self, new_velocity: f32) {
self.velocity = new_velocity;
}
pub fn set_y(&mut self, new_y: f32) {
self.y = new_y;
}
}
impl Obstacle {
pub fn new() -> Obstacle {
Obstacle {
x: SCREEN_HEIGHT - 10.,
}
}
pub fn set_x(&mut self, new_x: f32) {
self.x = new_x;
}
}
最後に、作ったPlaySystemと入力を受け付けるSystemをmain関数内のgame_dataに追加します。
// 入力システム
let input_bundle = InputBundle::<StringBindings>::new();
/**/
let game_data = GameDataBuilder::default()
.with_bundle(
RenderingBundle::<DefaultBackend>::new()
.with_plugin(
RenderToWindow::from_config_path(display_config_path)
.with_clear([0.34, 0.36, 0.52, 1.0]),
)
.with_plugin(RenderFlat2D::default()),
)?
.with_bundle(TransformBundle::new())?
.with_bundle(input_bundle)? // 入力システム追加
.with(PlaySystem, "play_system", &[]); // ゲームプレイシステム追加
####3-4-5. 完成
以下が今回のコードのまとめです。
use amethyst::{
assets::{AssetStorage, Handle, Loader},
core::{timing::Time, transform::TransformBundle, Transform},
ecs::prelude::*,
ecs::System,
input::{InputBundle, InputHandler, StringBindings, VirtualKeyCode},
prelude::*,
renderer::{
camera::Camera,
formats::texture::ImageFormat,
plugins::{RenderFlat2D, RenderToWindow},
sprite::{SpriteRender, SpriteSheet, SpriteSheetFormat},
types::DefaultBackend,
RenderingBundle, Texture,
},
utils::application_root_dir,
};
/// パラメータ
const SCREEN_WIDTH: f32 = 500.;
const SCREEN_HEIGHT: f32 = 500.;
const OBSTACLE_WIDTH: f32 = 303.;
const OBSTACLE_HEIGHT: f32 = 302.;
const ROCK_HEIGHT: f32 = 52.;
const GRAVITY: f32 = -0.5;
/// ゲームを実行するシステム
pub struct PlaySystem;
/// システムを作るときはSystemのTraitを用います
impl<'a> System<'a> for PlaySystem {
// このシステムで使用するComponentの種類
type SystemData = (
WriteStorage<'a, Transform>, // TransformはEntityの座標やサイズをそうさするComponentへの書き込み
WriteStorage<'a, Rock>, // 岩の情報を持ったComponentへの書き込み
WriteStorage<'a, Obstacle>, // 障害物の情報を持ったComponentへの書き込み
Read<'a, InputHandler<StringBindings>>, // ユーザーからの入力に関するComponentを読み込み
Read<'a, Time>, // 時間Componentの読み込み
);
/// システムの実行関数
fn run(&mut self, (mut transforms, mut rocks, mut obstacles, input, time): Self::SystemData) {
// joinによってEntityを共有しているComponentの集合を得ることが可能
for (transform, rock) in (&mut transforms, &mut rocks).join() {
// 前のフレームからの時間経過を取得
let dt = time.delta_real_seconds() * 70.;
// Enterキーの入力を検知
if input.key_is_down(VirtualKeyCode::Return) {
// 上向きへ速度を与えることでジャンプ
rock.set_velocity(7.);
}
// 基本的には下向きへの加速
let mut new_velocity = rock.velocity + GRAVITY * dt;
let mut new_y = rock.y + new_velocity * dt;
// 地面についたら速度が0になります
if new_y <= ROCK_HEIGHT / 2. {
new_velocity = 0.;
new_y = ROCK_HEIGHT / 2.;
}
// 実際に岩エンティティの座標を変更し、岩の情報も更新
transform.set_translation_y(new_y);
rock.set_y(new_y);
rock.set_velocity(new_velocity);
}
// 同様に障害物のComponentを取得
for (transform, obstacle) in (&mut transforms, &mut obstacles).join() {
// 左に進みます
let dt = time.delta_real_seconds() * 70.;
let mut new_x = obstacle.x - 5. * dt;
// 左端についたら右端へ移動させます
if new_x <= -OBSTACLE_WIDTH / 2. {
new_x = SCREEN_WIDTH;
}
obstacle.set_x(new_x);
transform.set_translation_x(new_x);
}
}
}
/// ゲーム画面のState
struct PlayState;
/// SimpleStateというTraitを使用
impl SimpleState for PlayState {
/// PlayStateへ遷移した瞬間に実行
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
// EntityをWorldに追加します
let sprite_sheet_handle = load_sprite_sheet(data.world);
set_camera(data.world);
set_rock(data.world, sprite_sheet_handle.clone());
set_obstacle(data.world, sprite_sheet_handle);
}
/// PlayStateがPopされるときに実行
fn on_stop(&mut self, data: StateData<'_, GameData<'_, '_>>) {
data.world.delete_all();
}
}
/// カメラEntityをWorldに追加
pub fn set_camera(world: &mut World) {
let mut camera_transform = Transform::default();
camera_transform.set_translation_xyz(SCREEN_WIDTH / 2., SCREEN_HEIGHT / 2., 1.0);
world
.create_entity()
.with(camera_transform)
.with(Camera::standard_2d(SCREEN_WIDTH, SCREEN_HEIGHT))
.build();
}
/// 岩EntityをWorldに追加
pub fn set_rock(world: &mut World, sprite_sheet_handle: Handle<SpriteSheet>) {
let mut rock_transform = Transform::default();
rock_transform.set_translation_xyz(SCREEN_WIDTH / 4., 0., 0.);
let rock_sprite_render = SpriteRender {
sprite_sheet: sprite_sheet_handle,
sprite_number: 0,
};
world
.create_entity()
.with(rock_transform)
.with(Rock::new())
.with(rock_sprite_render)
.build();
}
/// 障害物EntityをWorldに追加
pub fn set_obstacle(world: &mut World, sprite_sheet_handle: Handle<SpriteSheet>) {
let mut obstacle_transform = Transform::default();
obstacle_transform.set_translation_xyz(SCREEN_HEIGHT - 10., OBSTACLE_HEIGHT / 2. - 30., 0.);
let obstacle_sprite_render = SpriteRender {
sprite_sheet: sprite_sheet_handle,
sprite_number: 1,
};
world
.create_entity()
.with(obstacle_transform)
.with(Obstacle::new())
.with(obstacle_sprite_render)
.build();
}
/// 2Dゲームに必要な画像をまとめたファイルSpriteSheetをロード
pub fn load_sprite_sheet(world: &World) -> Handle<SpriteSheet> {
let loader = world.read_resource::<Loader>();
let texture_handle = {
let texture_storage = world.read_resource::<AssetStorage<Texture>>();
loader.load(
"texture/spritesheet.png",
ImageFormat::default(),
(),
&texture_storage,
)
};
let sprite_sheet_store = world.read_resource::<AssetStorage<SpriteSheet>>();
loader.load(
"texture/spritesheet.ron",
SpriteSheetFormat(texture_handle),
(),
&sprite_sheet_store,
)
}
/// 岩の情報を保持するComponent
pub struct Rock {
y: f32,
velocity: f32,
}
impl Rock {
pub fn new() -> Rock {
Rock {
y: 100.,
velocity: 0.,
}
}
pub fn set_velocity(&mut self, new_velocity: f32) {
self.velocity = new_velocity;
}
pub fn set_y(&mut self, new_y: f32) {
self.y = new_y;
}
}
/// ComponentというTraitを用いてRockをComponentにする
impl Component for Rock {
type Storage = DenseVecStorage<Self>;
}
/// 障害物の情報を保持するComponent
pub struct Obstacle {
x: f32,
}
impl Obstacle {
pub fn new() -> Obstacle {
Obstacle {
x: SCREEN_HEIGHT - 10.,
}
}
pub fn set_x(&mut self, new_x: f32) {
self.x = new_x;
}
}
/// ComponentというTraitを用いてObstacleをComponentにする
impl Component for Obstacle {
type Storage = DenseVecStorage<Self>;
}
fn main() -> amethyst::Result<()> {
amethyst::start_logger(Default::default());
let app_root = application_root_dir()?;
// スクリーンサイズなどの設定ファイルのパス
let config_dir = app_root.join("config");
let display_config_path = config_dir.join("display.ron");
// アセットのパス
let assets_dir = app_root.join("assets");
// 入力システム
let input_bundle = InputBundle::<StringBindings>::new();
// ゲームのデータを作成し、システムや設定を追加していく
let game_data = GameDataBuilder::default()
.with_bundle(
RenderingBundle::<DefaultBackend>::new()
.with_plugin(
RenderToWindow::from_config_path(display_config_path)
.with_clear([0.34, 0.36, 0.52, 1.0]),
)
.with_plugin(RenderFlat2D::default()), //フラットシェーディング
)?
.with_bundle(TransformBundle::new())?
.with_bundle(input_bundle)?
.with(PlaySystem, "play_system", &[]);
// アセットのパスと初期Stateとゲームデータによってゲームを作成
let mut game = Application::new(assets_dir, PlayState, game_data)?;
// ゲームの実行
game.run();
Ok(())
}
##4. まとめ
今回はAmethystの概要の紹介と、これから作っていくサンプルゲームの土台を作りました。興味を持った方は次回を楽しみにしていてください。
次回は、岩と障害物の当たり判定、岩のカメラスクロールを実装しようと思います。
##5. 筆者について
@takeryo_eeicというアカウントでTwitterをやっています。現在開発中のゲームに関して投稿したり、連載の更新をお知らせしたりするので、興味のある方はぜひフォローお願いします!
####引用
RustBook: https://doc.rust-jp.rs/book/second-edition/
Rustのメモリ関連: https://qiita.com/ksato9700/items/312be99d8264b553b193
ゲームエンジンの説明: https://geekly.co.jp/column/cat-webgame/1903_051/
Rustの概要: https://ja.wikipedia.org/wiki/Rust_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E)
MicrosoftのRustの実証実験: https://www.zdnet.com/article/microsofts-rust-experiments-are-going-well-but-some-features-are-missing/
Pistonの紹介: https://qiita.com/ryo33/items/cbe97fd4730027a55b10