虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

WebAssemblyで遊んでみるその4〜WebAssemblyで非同期通信して脱衣(仮)ブロック崩しにする〜

皆さんこんにちは。虎の穴ラボのY.Fです。

前回は番外編として、WebWorkerを使ったCanvas描画について書きました。

(前回の記事) toranoana-lab.hatenablog.com

今回はまた戻ってWebAssembly周りの記事となります。

ブロック崩しを作るにあたり、少し普通のものとは違うものにしたかったため、脱衣(仮)ブロック崩しを目指すことにしました。 脱衣(仮)ブロック崩しを作るには非同期通信(Ajax)でセル毎に分割した画像を逐次ロードする必要がありそうだったので、WebAssemblyでやってみました。

出来上がったもの

例のごとく出来上がったものを先に貼っておきます。

f:id:toranoana-lab:20191001144050g:plain

今回やりたいこと

一枚の画像をロードするようにしてしまうと、ブラウザの開発者ツールなどを使用して、画像のダウンロードができてしまうので、この方法はNGとしました。 したがって、今回やりたいことは以下になります。

  • 崩した部分のブロック範囲の画像をロードする
  • もともと出ている画像はボールがヒットしたらそのセル部分のみ削除したいので、これもセル分ロードする必要がある

流れは以下のような感じです。

f:id:toranoana-lab:20191001160843p:plain

今回は上記を以下のように切り分けます。

  • デフォルト画像のセル分ロード
    • たくさんあるのでWebAssembly側へ
  • レイヤ表示側の崩したブロックに対応する画像のロード
    • レイヤー側ワーカーでのfetchAPIリクエスト

デフォルト画像のセル分ロード(WebAssembly側)

前回までの記事と変わらず wasm-bindgen を使っています。これについて調べると、以下のページが見つかります。

rustwasm.github.io

This crate provides a bridge for working with JavaScript Promise types as a Rust Future, and similarly contains utilities to turn a rust Future into a JavaScript Promise.

とあるので、このクレートを利用することで、RustのFutureで非同期アクセスを書いて、それをJavaScriptのPromiseに変換できそうです。(ちなみに、RustのFutureについてはそこまで詳しくないです。)

素晴らしいことにサンプルもあります。

rustwasm.github.io

ということで、ソースです。ほぼサンプルそのままです。

use wasm_bindgen::prelude::*;
use futures::{future, Future};
use web_sys::{Request, RequestInit, RequestMode, Response, DedicatedWorkerGlobalScope, ImageBitmap};
use wasm_bindgen_futures::{JsFuture, future_to_promise};
use js_sys::Promise;
use wasm_bindgen::JsCast;

#[wasm_bindgen]
pub fn get_image(worker: &DedicatedWorkerGlobalScope, col: u32, row: u32, url: String) -> Promise {
    let mut opts = RequestInit::new();
    opts.method("GET");
    opts.mode(RequestMode::Cors);
    let request = Request::new_with_str_and_init(
        &format!("{}imgs/27_devil_{}_{}.png", url, row + 1, col + 1),
        &opts
    ).unwrap();
    let request_promise = worker.fetch_with_request(&request);

    let future = JsFuture::from(request_promise).and_then(|resp_value| {
        assert!(resp_value.is_instance_of::<Response>());
        let resp: Response = resp_value.dyn_into().unwrap();
        resp.blob()
    })
    .and_then(|blob: Promise| {
        JsFuture::from(blob)
    })
    .and_then(|blob| {
        future::ok(blob)
    });
        
    future_to_promise(future)
}
  • 引数に &DedicatedWorkerGlobalScope を取っている件
    • このRust関数側からは、実行コンテキストが何であるかはわかりません。それがメインスレッドなのか、ワーカーなのかは判別不能です。
    • 仮にワーカースレッドで実行している場合はグローバルのWindowオブジェクトは使えず、selfにDedicatedWorkerGlobalScopeが入ってきたりします。
    • そのため、コンテキストを明示的に引数で渡さないと、Windowにセットされているプロパティ(今回は fetch メソッドなど)が使えなくなってしまいます。
  • 戻り値 Promise
    • future_to_promise 関数によって future から promise に変換されるので、それを戻り値としています

TypeScript側から以下のような感じで使います。

public async bitmapInit() {
  for (let i = 0; i < brick_column_count(); i++) {
    for (let j = 0; j < brick_row_count(); j++) {
      // wasm側の関数
      get_image(self, i, j, ASSET_URL)
        .then(async (res: Blob) => {
          const bitmap = await createImageBitmap(res);
          this.bitmapArray[`${i}-${j}`] = bitmap;
        });
    }
  }
}

このあと、ロードした画像はCanvasに入れるため、 createImageBitmap で変換しておいて、何かしらの配列などに突っ込んでおきます。(何回も画像を取り直さないように)

Worker間の通信

お次は、ブロックを破壊したときの処理です。この処理に関しては、WebAssemblyに直接関係ありません。 単純にメインスレッドを経由してスレッド間でイベントを呼び出すだけです。

以下抜粋

(draw.worker.ts)

private update(width: number, height: number) {
  // これはwasm側の関数
  update_all(
    this.ball,
    this.paddle,
    this.status,
    this.container,
    width,
    height,
    // wasm側に与えるコールバックその1(ブロック破壊)
    (brickX: number, brickY: number) => {
      // js_sys::Funcrtionのapplyを使うのも面倒なので行と列番号は逆算する
      const c =
        (brickX - brick_offset_left()) / (brick_width() + brick_padding());
      const r =
        (brickY - brick_offset_top()) / (brick_height() + brick_padding());

      this.reply("collisionCallback", brickX, brickY, c, r);
    },
    // wasm側に与えるコールバックその2(クリアしたときなど)
    (msg: string) => {
      this.reply("alert", msg);
      cancelAnimationFrame(this.requestId);
      this.reply("terminate");
    }
  );
}

(main.ts)

private setupMainCanvas() {
  this.myWorker.addListeners("log", logFunc);

  this.myWorker.addListeners("alert", alertFunc);
  this.myWorker.addListeners(
    "collisionCallback",
    this.collisionCallback.bind(this)
  );
}

private collisionCallback(
  brickX: number,
  brickY: number,
  c: number,
  r: number
) {
  this.myLayer.sendQuery("drawImageCell", {
    brickX: brickX,
    brickY: brickY,
    c: c,
    r: r
  });
}

(layer.worker.ts)

const queryableFunctions: { [key: string]: any } = {
  drawImageCell: async (
    brickX: number,
    brickY: number,
    c: number,
    r: number
  ) => {
    logics.drawImageCell(brickX, brickY, c, r);
  },
  init: (canvas: OffscreenCanvas) => {
    logics.init(canvas);
  }
};

(draw-layer.worker.ts)

interface LogicsInterface {
  init: (canvas: OffscreenCanvas) => void;
  drawImageCell: (brickX: number, brickY: number, c: number, r: number) => void;
  canvas?: OffscreenCanvas;
}

export const logics: LogicsInterface = {
  init(canvas: OffscreenCanvas) {
    Object.assign(logics, { canvas: canvas });
  },

  async drawImageCell(brickX: number, brickY: number, c: number, r: number) {
    if (this.canvas && this.canvas instanceof OffscreenCanvas) {
      const ctxImage = this.canvas.getContext("2d");
      const imagePath = `${ASSET_URL}imgs/26_engel_${r + 1}_${c + 1}.png`;
      if (ctxImage && ctxImage instanceof OffscreenCanvasRenderingContext2D) {
        const res = await fetch(imagePath);
        const blob = await res.blob();

        const imageBitmap = await createImageBitmap(blob);
        ctxImage.drawImage(imageBitmap, brickX, brickY, 48, 20);
      }
    }
  }
};

そこまで難しいことはないと思います。 main.ts をバイパスして、最終的には draw-layer.worker.ts の関数が発火されます。
オブジェクトのプロパティに OffscreenCanvas を抱えているのは、毎回Transferしていると怒られたので初期化をして抱え込ませ、それを再利用するためです。

まとめ

虎の穴ラボの採用サイトに、今回までに作ったアプリを掲載致しましたので、ぜひ触ってみてください!

yumenosora.co.jp

今回紹介した内容は以下になります。

  • WebAssemblyを使用した非同期通信
  • Woker間の協調動作

ブロック崩しは今回で一段落して、今後はまた違ったものを作る過程でWebAssemblyを勉強していきたいと思います。

P.S

虎の穴では一緒に働く仲間を絶賛募集中です! この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。 yumenosora.co.jp

10/9(水)に秋葉原採用説明会を開催いたします。こちらも、ご興味のある方は是非ご応募ください!
なんと、今回はアーツ千代田様の会場を借りての開催となります! yumenosora.connpass.com