Yyatmita

【第15回】Bevy 製ゲームをブラウザで公開するまで — Rust/WASM 実配信ガイド

cargo build だけでは足りない。Bevy のゲームを wasm32 でビルドしてブラウザ配信するまでの実手順と詰まりどころ——webgl2・getrandom・audio autoplay・GitHub Pages まで。

Claude Codeでサイトをつくってみた#bevy#rust#wasm#game-dev#deployment#claude-code
← 前の記事: 【第14回】Bevy 入門:AI が書けない「今の書き方」で最小ゲームを動かす

この記事は何か

Bevy でゲームが動いた。次は人に遊んでもらう=ブラウザで公開です。ところが多くの チュートリアルは cargo run の localhost で終わり、配信で人は必ず詰まります。 真っ黒な画面、実行時 panic、モバイルでスクロールが暴発、音が鳴らない——。

この記事は、Rust 製ゲームエンジン Bevy の対戦大富豪を wasm32 でビルドしてブラウザ配信する までの実手順と詰まりどころを、実際の設定ファイルから残します。

1. ビルドの3段:build → glue → 最適化

wasm は「Rust を wasm にコンパイル → JS グルーを生成 → サイズ最適化」の3段です。

# 1) wasm ターゲットへリリースビルド
cargo build --release --target wasm32-unknown-unknown -p client
 
# 2) wasm-bindgen で JS グルー(web ターゲット)を生成
wasm-bindgen --out-dir . --target web --out-name grimoire_royale \
  target/wasm32-unknown-unknown/release/grimoire_royale.wasm
 
# 3) wasm-opt でサイズ最適化(← ここに罠。次項)

生成物は grimoire_royale.js(グルー)と grimoire_royale_bg.wasm(本体)。HTML から前者を import して初期化します。

wasm-opt -all を使ってはいけない-all は GC / 例外ハンドリング / reference-types まで 有効化し、実ブラウザで instantiate 不能なビルドが出来ます(詳細は姉妹記事「手触り編」§5)。 --enable-bulk-memory --enable-sign-ext --enable-mutable-globals --enable-nontrapping-float-to-int のように、rustc が実際に吐く feature だけを明示有効化。

2. Cargo の "ブラウザで動く" 設定(大半はここで詰まる)

cargo run(ネイティブ)では動くのに wasm で動かない、の正体はだいたいこの4つです。

[dependencies]
bevy = { version = "0.19", default-features = false, features = [
    # ... 使う機能だけ列挙してサイズを絞る ...
    "bevy_render", "bevy_sprite", "bevy_ui", "bevy_text", "bevy_winit",
    "png",
    "webgl2",   # ← これが無いとブラウザで「真っ黒」。WebGL2 バックエンド
] }
getrandom = "0.3"
 
[target.'cfg(target_arch = "wasm32")'.dependencies]
# wasm では OS の乱数が無い。wasm_js feature を付けないと実行時 panic
getrandom = { version = "0.3", features = ["wasm_js"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
console_error_panic_hook = "0.1"  # panic をブラウザの console に出す命綱

詰まりどころ:

  • webgl2 feature が無い → 画面が真っ黒(レンダラのバックエンドが選ばれない)
  • getrandomwasm_js feature が無い → 実行時に「unsupported」で panic。 乱数を使うゲーム(シャッフル・AI)は確実に踏む
  • panic が見えないconsole_error_panic_hook を main の頭でセットしないと、 wasm の panic は「無言で止まる」だけで原因が分からない
  • wasm 専用依存は [target.'cfg(target_arch = "wasm32")'.dependencies] に隔離 → ネイティブビルドを壊さない

3. index.html と "初回タップ"(ブラウザの都合)

HTML 側にもブラウザ特有の作法があります。

  • canvas を全画面+ touch-action: none → 指でゲームを操作するとき、モバイルの スクロール/ズームが暴発するのを止める
  • 音声は初回のユーザー操作まで鳴らせない(ブラウザの autoplay ポリシー)→ 「タップして開始」の consent モーダルを挟み、そこで AudioContext を起動する。 これを知らないと「実機で音だけ出ない」に悩む
  • ローディング表示を出す(wasm のフェッチ〜instantiate に一瞬かかる)
  • 配信段階なら <meta name="robots" content="noindex"> など運用メタも

4. 静的ホスティング(GitHub Pages)

Bevy の wasm ゲームは完全に静的(サーバー実行なし)なので、GitHub Pages で配信できます。 本作はデプロイスクリプトが、生成物を配信用の別リポジトリへ staging します。

  • index.html / grimoire_royale.js / grimoire_royale_bg.wasm を配信 repo にコピー
  • assets/(画像・音)を rsync(--delete で消えた素材も同期)
  • push は手動(意図しない公開を避ける)

補足: .wasmContent-Type: application/wasm で配信される必要があります(instantiateStreaming のため)。GitHub Pages は正しく返すので通常は気にせず済みます。自前サーバーだと MIME 設定を忘れがち。 マルチスレッド(SharedArrayBuffer)を使う場合のみ COOP/COEP ヘッダが要りますが、 シングルスレッドなら不要です。

5. バージョン刻印とキャッシュ

wasm は強くキャッシュされます。更新したのに古いままで「直ったはずのバグが出る」という 報告に悩まされないよう、配信ページに可視のビルドラベルvX.Y.Z (短縮ハッシュ 日付))を焼きます。

  • semver は git タグが正本、Conventional Commits から git-cliff が自動 bump (feat→minor / fix→patch、major は手動)。デプロイごとに自動タグ、push は手動
  • 不具合報告のスクショにラベルが写るので、どのコミットの版かが一目で分かる

6. 詰まりどころ早見表

症状原因対処
画面が真っ黒webgl2 feature 無しbevy features に webgl2
実行時に無言で停止乱数 panicgetrandomwasm_js feature
原因が全く分からないpanic がコンソールに出ないconsole_error_panic_hook を main で set
ブラウザで instantiate 失敗wasm-opt -all必要 feature だけ明示有効化
モバイルでスクロール暴発canvas の touch 挙動touch-action: none
実機で音だけ出ないautoplay ポリシー初回タップで AudioContext 起動
直したのに古いままwasm キャッシュ版ラベル可視化+キャッシュ更新

つぎは:手触り(game feel)

公開できたら、次は「触っていて気持ちいい」を足す番です。ヒットストップ・スクリーンシェイク・ 被弾フラッシュを Bevy でどう実装したか——そして なぜ AI/LLM はそれを黙って省くのかは、 姉妹記事へ:

AI にゲームは作れる。でも「手触り」は作れない


参考

  • Bevy 公式 / Bevy Cheat Book(WASM デプロイの節)
  • wasm-bindgen / wasm-opt(binaryen)の各ドキュメント
  • 本記事の実設定: grimoire-royale の crates/client/Cargo.toml / index.html / scripts/build-deploy.sh