Rustとquicksilverでwasmゲームを作ってみる

## つくったもの

FlappyBirdのFerrisバージョン。名付けてFlappyFerris。

![FlappyFerris.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44173/5cc4f4ee-1b7e-5148-7944-052bfce3bfd2.gif)

<a href="https://github.com/yukinarit/flappy_ferris"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44173/548a42ac-6456-519e-5262-59c0b3b6ada4.png" width="460px"></a>

## `quicksilver`とは

[quicksilver](https://github.com/ryanisaacg/quicksilver)とはRustで書かれた2Dゲームエンジンで、Web(wasm)、Desktop(Windows, macOS, Linux)に対応したクロスプラットフォームのゲームを作ることができる。Gitの履歴によると、2018年10月に開発が始まって2019/12/9時点でver0.3.2なので、まだ比較的新しいゲームエンジンといえる。

[公式ドキュメント](https://ryanisaacg.com/quicksilver/)によると、以下の機能が売りのようだ。

* 2Dジオメトリ（ベクタ、長方形、円等、図形処理）
* キーボードと3つのマウスボタンサポート
* OpenGLベースの描画
* 多くの画像フォーマット
* サウンド
* 非同期アセット読み込み
* デスクトップとWebの統一的なコードベース
* [ncollide2d](https://github.com/rustsim/ncollide)ベースのコリジョン
* [rusttype](https://github.com/redox-os/rusttype)によるTTFフォント
* [girls](https://gitlab.com/gilrs-project/gilrs)によるゲームパッド対応
* [serde_json](https://github.com/serde-rs/json)を使ったセーブ機能
* [lyon](https://github.com/nical/lyon)による複雑な図形とSVGレンダリング
* [immi](https://github.com/tomaka/immi)によるImmediate-mode GUIs

筆者的には、quicksilverは後発で[Amethyst](https://amethyst.rs/)や[Piston](https://github.com/PistonDevelopers/piston)なんかと比べて開発もまだそんなに活発ではないんだけど、最初からwasmをターゲットにしてるシンプルなゲームエンジンとして十分に価値があると思っている。実際、筆者はwasmに詳しくないがゲームを作ることができた。

## Setup

### 依存パッケージのインストール

* Debian / Ubuntu

```sh
sudo apt install libudev-dev zlib1g-dev alsa libasound2-dev
```

* CentOS / RHEL

```sh
sudo yum install systemd-devel zlib-devel alsa-lib-devel
```

* macOS

```sh
brew install zlib
```

### cargo-webのインストール

```sh
cargo install cargo-web
```

## Run

* flappy_ferryをclone

```sh
git clone https://github.com/yukinarit/flappy_ferris
cd flappy_ferris
```

* wasmとして実行する

```sh
cargo web start
```

* デスクトップアプリとして実行する

```sh
cargo run
```

## Hello World

```toml
[package]
name = "flappy_ferris"
version = "0.1.0"
authors = ["yukinarit"]
edition = "2018"

[dependencies]
quicksilver = "*"
```

```rust
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するとゲームオブジェクトとなり、ゲームループに対する更新処理を書ける。

```rust
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`: マウスイベントなどのイベントハンドラ。イベントの種類は[こちら](https://docs.rs/quicksilver/0.3.20/quicksilver/lifecycle/enum.Event.html)
* `handle_error`: エラーのハンドラ

## 図形描画

[`Drawable`](https://docs.rs/quicksilver/0.3.20/quicksilver/graphics/trait.Drawable.html?search=)トレイトをimplしているCircle, Rectangle, Line, Triangle等の[型](https://docs.rs/quicksilver/0.3.20/quicksilver/graphics/trait.Drawable.html?search=#implementors)が用意されていて、[Window::draw](https://docs.rs/quicksilver/0.3.20/quicksilver/lifecycle/struct.Window.html#method.draw)にぶち込むだけで描画してくれる。

```rust
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(())
    }
}
```

![Screenshot from 2019-12-19 12-49-33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44173/d3667478-9bea-4cae-234d-152b3816609b.png)


## アセット / スプライト

読み込み用の型(例えば画像なら[Image](https://docs.rs/quicksilver/0.3.20/quicksilver/graphics/struct.Image.html))が用意されていて、[Asset](https://docs.rs/quicksilver/0.3.20/quicksilver/lifecycle/struct.Asset.html)クラスで非同期読み込みできる。

```rust
let img = Asset::new(Image::load("ファイルパス"));
img.execute(|img| {
    // ImageがロードされていたらこのClosureが実行される
});
img.execute_or(
    |img| { // ImageがロードされていたらこのClosureが実行される },
    || { // ImageがまだロードされていなかったらこのClosureが実行される },
);
```

## 入力

マウスとキーイベントなら`State`traitの`event`に渡される[`Event`](https://docs.rs/quicksilver/0.3.20/quicksilver/lifecycle/enum.Event.html)型から取ることができる。

例えば、マウスクリックされたらプレイヤーの位置を変更するにはこんな感じのコードを書けばいい

```rust
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とか使って切り替えれば良さそう。

```rust
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を切り換える様子

![chrome-capture.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44173/d3d3b95b-5d5c-4c5e-2d1d-affb47620f13.gif)


## Logging

Rustでは[log](https://docs.rs/log/0.4.10/log/)とその実装である[env_logger](https://docs.rs/env_logger/0.7.1/env_logger/)や[slog](https://docs.rs/slog/2.5.2/slog/)等がよくセットで使われるんだけど、うっかりquicksilverで使っちゃうとwasm版の時だけランタイムエラーになってしまうので、wasmに対応したロガーを使おう。このあたり、wasmで対応してないcrateつかったらコンパイルエラーなってくれるといいんだけどなぁ。

wasm対応してそうなlogベースのロガーはこのあたり

* [stdweb-logger](https://crates.io/crates/stdweb-logger)
* [wasm-logger](https://crates.io/crates/wasm-logger)

デスクトップ、wasm両方で開発する場合は、こんなラッパーを作るといい。

```rust
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もだしてくれるので、だいたいのあたりはつけられる。

![Screenshot from 2019-12-19 13-04-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44173/133d7353-53bb-52a8-a39a-991273edc402.png)


## 今回できなかったところ

### 衝突判定

[ncollide2](https://github.com/rustsim/ncollide)を使えばいいらしい


### アニメーション

quicksilverの中にある[Animation実装](https://docs.rs/quicksilver/0.3.20/quicksilver/graphics/struct.Animation.html)はv0.3.20時点でDEPRECATEDになっている。おそらく、[tweek-rust](https://medium.com/@hughlang/introducing-tweek-a-tween-animation-kit-for-rust-7d6f1ce7503d)辺りを使えばいいのではないかな。

## それで、quicksilverどうなの？

quicksilverはwasm周りに詳しくなくても割と簡単にwasmゲームが作れるという意味ではいいゲームエンジンだと思う。ただし、AmethystやPistonと比べるとまだまだ機能やサンプルが少なく、凝ったことをしようとすると自分やり方を見つけないといけないので、自分で好きなライブラリを組み合わせたり、quicksilver自体にコントリビュートしたい人にはオススメだと思う。