LoginSignup
16
8

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-25

つくったもの

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

FlappyFerris.gif

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は後発でAmethystPistonなんかと比べて開発もまだそんなに活発ではないんだけど、最初から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(())
    }
}

Screenshot from 2019-12-19 12-49-33.png

アセット / スプライト

読み込み用の型(例えば画像ならImage)が用意されていて、Assetクラスで非同期読み込みできる。

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

入力

マウスとキーイベントならStatetraitの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を切り換える様子

chrome-capture.gif

Logging

Rustではlogとその実装であるenv_loggerslog等がよくセットで使われるんだけど、うっかり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もだしてくれるので、だいたいのあたりはつけられる。

Screenshot from 2019-12-19 13-04-41.png

今回できなかったところ

衝突判定

ncollide2を使えばいいらしい

アニメーション

quicksilverの中にあるAnimation実装はv0.3.20時点でDEPRECATEDになっている。おそらく、tweek-rust辺りを使えばいいのではないかな。

それで、quicksilverどうなの?

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

16
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
8