【第15回】Bevy 製ゲームをブラウザで公開するまで — Rust/WASM 実配信ガイド
cargo build だけでは足りない。Bevy のゲームを wasm32 でビルドしてブラウザ配信するまでの実手順と詰まりどころ——webgl2・getrandom・audio autoplay・GitHub Pages まで。
← 前の記事: 【第14回】Bevy 入門:AI が書けない「今の書き方」で最小ゲームを動かすこの記事は何か
Bevy でゲームが動いた。次は人に遊んでもらう=ブラウザで公開です。ところが多くの
チュートリアルは cargo run の localhost で終わり、配信で人は必ず詰まります。
真っ黒な画面、実行時 panic、モバイルでスクロールが暴発、音が鳴らない——。
この記事は、Rust 製ゲームエンジン Bevy の対戦大富豪を wasm32 でビルドしてブラウザ配信する までの実手順と詰まりどころを、実際の設定ファイルから残します。
- 対象: Bevy でゲームを作り始めた人/AI に作らせたものを公開したい人
- 前提(作り方の基礎): Bevy 入門:AI が書けない「今の書き方」で最小ゲームを動かす
- 遊べる完成形: /games/grimoire-royale/
- 姉妹記事(手触り編): 「手触り(game feel)」をどう足したかは AI にゲームは作れる。でも「手触り」は作れない へ
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 に出す命綱詰まりどころ:
webgl2feature が無い → 画面が真っ黒(レンダラのバックエンドが選ばれない)getrandomのwasm_jsfeature が無い → 実行時に「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 は手動(意図しない公開を避ける)
補足:
.wasmはContent-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 |
| 実行時に無言で停止 | 乱数 panic | getrandom に wasm_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 はそれを黙って省くのかは、 姉妹記事へ:
参考
- Bevy 公式 / Bevy Cheat Book(WASM デプロイの節)
wasm-bindgen/wasm-opt(binaryen)の各ドキュメント- 本記事の実設定: grimoire-royale の
crates/client/Cargo.toml/index.html/scripts/build-deploy.sh