LoginSignup
20
13

More than 3 years have passed since last update.

rust + WebAssembleyでライフゲーム

Last updated at Posted at 2020-06-24

rustとWebAssembleyで何か作ろうと思ったんですが、何も思い浮かばず、チュートリアルをやってみました。

チュートリアルをやるとこんな感じのライフゲームが作れます。
wasm.gif
デモはこちらにあります。
https://wasm-rust-game-of-life.netlify.app/

コードはこちらです。
https://github.com/pokotyan/wasm-game-of-life

WebAssembley

ブラウザ上で動かせるバイナリです。C、C++、Rust、Go、Kotlin、AssemblyScript(TypeScriptのサブセット)などからコンパイルして生成することができます。
いろんな言語からwasmを作れますが、GCを備えた言語だとコンパイル後のwasmファイルが大きくなってしまいます。RustのようなGCがない言語だとwasmのサイズが小さくなるため、ページ読み込み速度も改善するようです。参考

wasmの現状の主な用途は重い計算処理などをオフロードさせるために使われてるみたいなので、GCがないかつ、メモリ安全なRustは相性いいんじゃないかなと思いました。

ツールチェイン

rustでWebAssembleyをやる際のツールチェインはこちらの記事がよく纏まっていました。大きく分けてemscripten系統とwasm-bindgen系統に大別されるみたいです。(wasm-bindgenの方が新しい)

冒頭のチュートリアルではwasm-bindgen系統であるwasm-packを利用しています。
wasm-bindgenはRust(wasm)とjs間がやりとりできるようなインターフェースを作ってくれるツールですが、wasm-packはそれをさらにwrapして、webpackで読み込めるようにしたりとか、npmに簡単にpublishできるようなコマンドを用意してくれたりしてます。

ボイラープレート

チュートリアルでは以下のボイラープレートを用いて、さらに環境構築を簡単にしています。

rust(wasm)側のテンプレート

wasm-pack-template
https://github.com/rustwasm/wasm-pack-template

wasm-packを活用したボイラープレートです。このテンプレートをcargo-generateコマンドを用いて環境構築します。
rustのパッケージマネージャであるcargoはいろいろなサブコマンドが用意されており(自分で作ることも可能)、cargo-generateを使うと、指定したgitリポジトリのテンプレートを元にRustのプロジェクトを新規作成できます。

なので、これを一発打つだけで、Rust側の環境構築は終わりです。
(もちろん事前にrustやcargoのインストールは必要ですが、そこもチュートリアル上でちゃんと書かれてます)

$ cargo generate --git https://github.com/rustwasm/wasm-pack-template

フロント側のテンプレート

create-wasm-app
https://github.com/rustwasm/create-wasm-app

このテンプレートを npm init wasm-app で利用すると hello-wasm-pack というwasmのnpmパッケージを利用する最低限のプロジェクトが生成されます。

import * as wasm from "hello-wasm-pack";

wasm.greet();

チュートリアルでは、ここのimportを実際に自分でコーディングし、ビルドしたrustのwasmに置き換えながら進めていきます。

他にもあるテンプレート

上記の wasm-pack-template と create-wasm-app のテンプレートは以下のような利用を想定したテンプレートです。

  • wasm-pack-template
    Rustで作ったwasmをnpmにパブリッシュし、npmパッケージにする。

  • create-wasm-app
    npm上にあるwasmをインストールし利用する。

なので、どちらもnpmを介して利用されることを期待したテンプレートです。
そうではなく、モノレポ的に使えるオールインワンなテンプレートも用意されてます。

rust-webpack-template
https://github.com/rustwasm/rust-webpack-template

rust-parcel-template
https://github.com/rustwasm/rust-parcel-template

この二つの違いは名前の通り、バンドラーにwebpackを使うかparcelを使うかくらいの違いだとは思いますが、これを使えば一瞬でRust + WebAssembley + jsの環境構築ができるので、wasmをちょっと試したい場合にはすごい便利そうです。

ライフゲーム

ライフゲームでは各セルが生死の状態を持ちます。
そしてそれぞれのセルが以下の4つの条件によって生死を繰り返します。

  • 生きている隣人が2つ以下の生細胞は、過疎化が原因であるかのように、死ぬ。
  • 2つまたは3つの生きた隣人を持つすべての生細胞は、次の世代に生き続ける。
  • 3つ以上の生きている隣人を持つ生きている細胞は、過疎が原因であるかのように、死ぬ。
  • 生きている隣人がちょうど3人いる死んだ細胞は、生殖によって生じたかのように、生きている細胞になる。

この世代交代をrequestAnimationFrameの度に実行すると、冒頭に貼ったgifのようになります。
このライフゲームで現れるセルのパターンはいろいろあり、調べると色々出てきます。僕はこのチュートリアルでライフゲームというものを知りましたが、ライフゲームはチューリング完全らしく、論理ゲートを作ってる人もいました

jsからwasmのメモリを参照する

チュートリアルでは、初めは簡単な実装でライフゲームを動かし、それをリファクタしていく形で進んでいきます。

最初の実装はrustのwasm側で「セルの生死の状態によって ◼ もしくは ◻ を出力する」というコードを書きます。そしてjs側からはその関数を呼び出すだけです。js側からwasm側に全ての処理を任せてるイメージです。

lib.rs
#[wasm_bindgen]
impl Universe {
    // ...

    pub fn render(&self) -> String {
        self.to_string()
    }
}

impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for line in self.cells.as_slice().chunks(self.width as usize) {
            for &cell in line {
                // cellの状態によって表示を変える
                let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
                write!(f, "{}", symbol)?;
            }
            write!(f, "\n")?;
        }

        Ok(())
    }
}
index.js
const pre = document.getElementById("game-of-life-text");

// js側からはrenderを叩くだけ。
pre.textContent = universe.render();

それを次の実装ではjs側からwasmのメモリを参照して各セルの生死の状態を確認し、js側で描画するといった形にリファクタします。

具体的にはこんな感じでwasmのメモリを参照しています。

lib.rs
#[wasm_bindgen]
impl Universe {
    // cellsには各セルの生死状態(0 or 1)が入った配列。それのポインタを返す
    pub fn cells(&self) -> *const Cell {
        self.cells.as_ptr()
    }
index.js
import { Universe, Cell } from "wasm-game-of-life";
import { memory } from "wasm-game-of-life/wasm_game_of_life_bg";

  // wasmのメモリ内からcellsのメモリ内容を取得する。
  // cellsPtrにはcellsの先頭アドレスが入っている。cellsの先頭からwidth * height分の範囲のメモリ内容を返す。
  const cellsPtr = universe.cells();
  const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);

そして、wasmのメモリから取り出した各セルの情報を元に画面へ描画します。

index.js
  ctx.beginPath();

  for (let row = 0; row < height; row++) {
    for (let col = 0; col < width; col++) {
      const idx = getIndex(row, col);

      // cellの状態によって表示を変える
      ctx.fillStyle = cells[idx] === Cell.Dead ? DEAD_COLOR : ALIVE_COLOR;

      ctx.fillRect(
        col * (CELL_SIZE + 1) + 1,
        row * (CELL_SIZE + 1) + 1,
        CELL_SIZE,
        CELL_SIZE
      );
    }
  }

  ctx.stroke();

今まで、jsでArrayBufferを扱うコードを書くことはそんななかったため、あまりわかってないですが、ここら辺のメモリの扱いがWebAssembleyをやってく上でのキモだなあという感じがしています。

最後に

僕はもういいかと思ってやってないですが、 以下のような内容もチュートリアルにあります。

  • 生死の判定に8bitも使っているのを1bitで判定するようにリファクタ
  • テストの追加
  • フレームのたびにデバッガーを挟む
  • 一時停止ボタン
  • フレームごとにかかった処理時間の計測
  • ビルドのオプションを見直して、wasmのサイズ削減
  • npmへpublish

そんなに時間もかからず終わるのでWebAssembleyが気になっている人はこのチュートリアルをやってみてはどうでしょうか!

20
13
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
20
13