つくったもの
FlappyBirdのFerrisバージョン。名付けてFlappyFerris。
quicksilver
とは
quicksilverとはRustで書かれた2Dゲームエンジンで、Web(wasm)、Desktop(Windows, macOS, Linux)に対応したクロスプラットフォームのゲームを作ることができる。Gitの履歴によると、2018年10月に開発が始まって2019/12/9時点でver0.3.2なので、まだ比較的新しいゲームエンジンといえる。
公式ドキュメントによると、以下の機能が売りのようだ。
- 2Dジオメトリ(ベクタ、長方形、円等、図形処理)
- キーボードと3つのマウスボタンサポート
- OpenGLベースの描画
- 多くの画像フォーマット
- サウンド
- 非同期アセット読み込み
- デスクトップとWebの統一的なコードベース
- ncollide2dベースのコリジョン
- rusttypeによるTTFフォント
- girlsによるゲームパッド対応
- serde_jsonを使ったセーブ機能
- lyonによる複雑な図形とSVGレンダリング
- immiによるImmediate-mode GUIs
筆者的には、quicksilverは後発でAmethystやPistonなんかと比べて開発もまだそんなに活発ではないんだけど、最初からwasmをターゲットにしてるシンプルなゲームエンジンとして十分に価値があると思っている。実際、筆者はwasmに詳しくないがゲームを作ることができた。
Setup
依存パッケージのインストール
- Debian / Ubuntu
sudo apt install libudev-dev zlib1g-dev alsa libasound2-dev
- CentOS / RHEL
sudo yum install systemd-devel zlib-devel alsa-lib-devel
- macOS
brew install zlib
cargo-webのインストール
cargo install cargo-web
Run
- flappy_ferryをclone
git clone https://github.com/yukinarit/flappy_ferris
cd flappy_ferris
- wasmとして実行する
cargo web start
- デスクトップアプリとして実行する
cargo run
Hello World
[package]
name = "flappy_ferris"
version = "0.1.0"
authors = ["yukinarit"]
edition = "2018"
[dependencies]
quicksilver = "*"
use quicksilver::{Result, geom::Vector, lifecycle::{Settings, State, run}};
struct HelloWorld;
impl State for HelloWorld {
fn new() -> Result<HelloWorld> {
Ok(HelloWorld)
}
}
fn main() {
run::<HelloWorld>("HelloWorld", Vector::new(800, 600), Settings::default());
}
ゲームオブジェクト
State
をimplするとゲームオブジェクトとなり、ゲームループに対する更新処理を書ける。
pub trait State: 'static {
fn new() -> Result<Self> where Self: Sized;
fn update(&mut self, _window: &mut Window) -> Result<()> { ... }
fn event(&mut self, _event: &Event, _window: &mut Window) -> Result<()>; { ... }
fn draw(&mut self, window: &mut Window) -> Result<()> { ... }
fn handle_error(error: Error) { ... }
}
-
new
: オブジェクト生成 -
update
: ゲームループ毎に呼ばれるハンドラ。ゲームオブジェクトの更新処理を書く -
draw
: 描画タイミングで呼ばれるハンドラ。ゲームオブジェクトの描画処理を書く -
event
: マウスイベントなどのイベントハンドラ。イベントの種類はこちら -
handle_error
: エラーのハンドラ
図形描画
Drawable
トレイトをimplしているCircle, Rectangle, Line, Triangle等の型が用意されていて、Window::drawにぶち込むだけで描画してくれる。
use quicksilver::{geom::{Circle, Line, Rectangle, Transform, Vector},
graphics::{Background::Col, Color}, lifecycle::{run, Settings, State, Window}, Result
};
impl State for HelloWorld {
fn draw(&mut self, w: &mut Window) -> Result<()> {
w.draw(&Rectangle::new((100, 100), (32, 32)), Col(Color::BLUE));
w.draw_ex(&Rectangle::new((400, 300), (32, 32)), Col(Color::BLUE), Transform::rotate(45), 10);
w.draw(&Circle::new((400, 300), 100), Col(Color::GREEN));
w.draw_ex(&Line::new((50, 80), (600, 450)).with_thickness(2.0), Col(Color::RED), Transform::IDENTITY, 5);
Ok(())
}
}
アセット / スプライト
読み込み用の型(例えば画像ならImage)が用意されていて、Assetクラスで非同期読み込みできる。
let img = Asset::new(Image::load("ファイルパス"));
img.execute(|img| {
// ImageがロードされていたらこのClosureが実行される
});
img.execute_or(
|img| { // ImageがロードされていたらこのClosureが実行される },
|| { // ImageがまだロードされていなかったらこのClosureが実行される },
);
入力
マウスとキーイベントならState
traitのevent
に渡されるEvent
型から取ることができる。
例えば、マウスクリックされたらプレイヤーの位置を変更するにはこんな感じのコードを書けばいい
fn event(&mut self, event: &Event, _: &mut Window) -> Result<()> {
match event {
Event::MouseButton(_, ButtonState::Released) => {
self.player.pos.y -= 64.0;
}
_ => {}
}
Ok(())
}
シーンの遷移
シーンを切り替えるような動作はエンジンから提供されていないが、単純にState
をimplしているオブジェクトをenumやBoxとか使って切り替えれば良さそう。
enum Scenes {
S1(Scene1),
S2(Scene2),
}
impl State for Scenes {
fn new() -> Result<Scenes> {
Ok(Scenes::S1(Scene1::new()?))
}
fn event(&mut self, event: &Event, _: &mut Window) -> Result<()> {
if let Event::MouseButton(_, ButtonState::Released) = event {
match self {
Scenes::S1(_) => {
// Scene1でマウスクリックされたらScene2に切り換える
std::mem::replace(self, Scenes::S2(Scene2::new()?));
}
Scenes::S2(_) => {
// Scene2でマウスクリックされたらScene1に切り換える
std::mem::replace(self, Scenes::S1(Scene1::new()?));
}
}
}
Ok(())
}
fn draw(&mut self, w: &mut Window) -> Result<()> {
match self {
Scenes::S1(s) => s.draw(w),
Scenes::S2(s) => s.draw(w),
}
}
}
struct Scene1;
struct Scene2;
分かりづらいけど、クリックでScene1, Scene2を切り換える様子
Logging
Rustではlogとその実装であるenv_loggerやslog等がよくセットで使われるんだけど、うっかりquicksilverで使っちゃうとwasm版の時だけランタイムエラーになってしまうので、wasmに対応したロガーを使おう。このあたり、wasmで対応してないcrateつかったらコンパイルエラーなってくれるといいんだけどなぁ。
wasm対応してそうなlogベースのロガーはこのあたり
デスクトップ、wasm両方で開発する場合は、こんなラッパーを作るといい。
fn init_logger() {
// wasmの場合、stdweb_loggerを使う
#[cfg(target_arch = "wasm32")]
stdweb_logger::init();
// Desktopの場合、env_loggerを使う
#[cfg(not(target_arch = "wasm32"))]
env_logger::from_env(env_logger::Env::default().default_filter_or("info")).init();
}
fn main() {
init_logger();
log::info!("foo");
run::<HelloWorld>("HelloWorld", Vector::new(800, 600), Settings::default());
}
デバッグ
例えばログの例だとエラーの内容はConsoleにだしてくれるのと、エラーメッセージをクリックするとさらにBacktraceもだしてくれるので、だいたいのあたりはつけられる。
今回できなかったところ
衝突判定
ncollide2を使えばいいらしい
アニメーション
quicksilverの中にあるAnimation実装はv0.3.20時点でDEPRECATEDになっている。おそらく、tweek-rust辺りを使えばいいのではないかな。
それで、quicksilverどうなの?
quicksilverはwasm周りに詳しくなくても割と簡単にwasmゲームが作れるという意味ではいいゲームエンジンだと思う。ただし、AmethystやPistonと比べるとまだまだ機能やサンプルが少なく、凝ったことをしようとすると自分やり方を見つけないといけないので、自分で好きなライブラリを組み合わせたり、quicksilver自体にコントリビュートしたい人にはオススメだと思う。