Bevyでゲームを作る:RustからWebAssemblyへの道
遊びをつくる執筆にClaudeを使用しています。

BevyはRust製のオープンソースゲームエンジンです。ECS(Entity Component System)アーキテクチャを採用しており、C++やGDScriptではなくRustで記述できることが最大の特徴です。そしてRustはWebAssembly(以下Wasm)へのコンパイルを標準的にサポートしているため、Bevyで作ったゲームをブラウザで動かす道が整っています。 「RustでゲームをつくってWebに出したい」と考えたとき、まず直面するのは「本当にBevyはブラウザで動くのか」「どこで詰まるのか」という疑問です。結論から言うと、動かすこと自体は可能ですが、ネイティブビルドと同じ感覚で進めると複数の壁にぶつかります。本記事ではその壁の場所と越え方を順に整理します。
BevyとWasmの関係を把握する
Bevyのビルドターゲットとしてwasm32-unknown-unknownが公式にサポートされています。ただし、「サポートされている」は「自動でうまくいく」を意味しません。Bevyのクレート(ライブラリ)の中にはWasmで動作しないものや、別の設定が必要なものが混在しています。
特に注意が必要なのは次の点です。
- 非同期ランタイム:BevyはデスクトップではTokioなどの非同期ランタイムを使う場面がありますが、Wasmではブラウザのイベントループに乗せる必要があります。bevyクレートのデフォルト機能フラグにはmulti_threadedがあり、Wasmでは無効化しなければなりません。
- ファイルI/O:デスクトップではファイルシステムからアセットを読み込みますが、Wasmにはファイルシステムがありません。代わりにHTTPリクエストでアセットを取得する仕組みに切り替わります。
- ウィンドウとCanvas:BevyのWindowPluginはWasm向けにcanvas要素と接続する設定が必要です。
これらを踏まえて、「まず動かす最小構成」から始めるのが現実的な進め方です。
環境を整える
RustとCargoがインストール済みであることを前提とします。まず必要なターゲットを追加します。
rustup target add wasm32-unknown-unknown次に、WasmバイナリをJavaScript/HTMLから使いやすい形にまとめるツールとしてwasm-bindgen-cliとwasm-pack、あるいはtrunkを用意します。Bevy向けに最も手軽なのはTrunkです。Trunkはcargo buildに相当するビルドとHTMLバンドルを一括で行い、ローカル開発用のサーバーも内蔵しています。
cargo install trunkCargo.tomlのBevy依存には、Wasm非対応のデフォルト機能を外し、必要なものだけ有効にします。
[dependencies]
bevy = { version = "0.14", default-features = false, features = [
"bevy_winit",
"bevy_render",
"bevy_core_pipeline",
"bevy_sprite",
"bevy_asset",
"bevy_text",
"png",
"webgl2",
] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }webgl2フィーチャーを指定するのはWasm向けのレンダラーを明示的に有効にするためです。BevyのデフォルトレンダラーであるwgpuはWebGPUにも対応しつつありますが、2026年時点でも安定したブラウザ対応を求めるならWebGL2が無難な選択です。
プロジェクトルートにindex.htmlを置き、Trunkが差し込む形でWasmモジュールを読み込む最小構成にします。<link data-trunk rel="rust" />という一行をbody内に置くだけで、TrunkがWasmビルドとJSグルーコードの生成を処理します。
アセット読み込みの設計を変える
デスクトップ向けBevyゲームではassets/フォルダをバイナリと並べて配置するだけでよいですが、Wasmではブラウザ経由でファイルを取得します。TrunkはCargoのビルドに合わせてassets/ディレクトリをそのままWebルートに配置してくれますが、コード側の設定が必要です。
Appの構築時に、Wasm向けのアセットサーバー設定を切り替えます。条件コンパイルを使い、ネイティブとWasmでAssetPluginの挙動を分岐させるのが一般的なやり方です。
#[cfg(target_arch = "wasm32")]
app.add_plugins(AssetPlugin {
asset_folder: "assets".to_string(),
..default()
});また、非同期のアセット読み込みはブラウザでは完了に時間がかかります。「ロード中に別のシステムが動いてしまいパニックになる」というバグはWasm固有のものではありませんが、ブラウザでは通信遅延が加わるため頻度が上がります。AssetServerのload_stateを確認してからゲームシーンに遷移するロードステートを設けることで、このバグを防げます。
ビルドと動作確認の手順
開発中の確認は次のコマンド一つで行えます。
trunk serveデフォルトではlocalhost:8080に開発サーバーが立ち上がります。ホットリロードは対応していませんが、ファイルを変更して保存するとTrunkがリビルドをトリガーします。Rustのコンパイルはネイティブと比べてWasm向けの方が時間がかかる傾向があります。デバッグビルドでも数十秒かかることがあるため、開発サイクルを速くするには変更対象のシステムを小さく保つことが有効です。
リリース用ビルドは次のように行います。
trunk build --releasedist/フォルダにHTMLとWasmバイナリ、JavaScriptグルーコード、アセット類が出力されます。このフォルダをそのままWebサーバーにアップロードすれば公開できます。アソバースではZIPファイルの直接投稿に対応しているため、dist/フォルダをZIP圧縮して投稿することでブラウザゲームとして公開できます。
詰まりやすいポイントと対処
パニックメッセージが読めない問題:Wasmでパニックが起きるとブラウザのコンソールに出力されますが、デフォルトでは情報が少ないです。console_error_panic_hookクレートを開発用依存に追加し、mainの先頭でconsole_error_panic_hook::set_once()を呼ぶことで、Rustのパニックメッセージをコンソールに表示できます。
音声の自動再生制限:ブラウザはユーザー操作なしに音声を再生させません。Bevy標準のAudioPluginがWasmで動作する場合も、初回再生はクリックなどのインタラクションの後に行う設計にする必要があります。
Wasmバイナリのサイズ:Bevyのリリースビルドはバイナリが大きくなります。Cargo.tomlの[profile.release]にopt-level = "s"(サイズ最適化)やlto = true(リンク時最適化)を設定すると削減できます。wasm-optツール(Binaryenに含まれる)を後処理として走らせるとさらに縮小できます。Trunkはwasm-optがインストールされていれば自動で呼び出します。
マルチスレッド機能の無効化忘れ:Cargo.tomlでBevyのmulti_threadedフィーチャーを残したままWasm向けビルドをするとコンパイルエラーになります。Wasm向けのフィーチャーリストには含めないことを確認してください。
次の行動
まずtrunk serveで動く最小のBevyプロジェクトを手元に作り、画像一枚を表示するところから始めるのが確実な出発点です。dist/フォルダが出力できたら、ZIPに圧縮してアソバース(https://aso-verse.com)に投稿することで、すぐにブラウザ上で動作を確認できます。Bevyのバージョンアップごとにフィーチャーフラグや設定が変わることがあるため、公式ドキュメントとマイグレーションガイドをその都度確認してください。
参考資料
- Bevy公式ドキュメント(bevy_app, bevy_asset, WebGL2フィーチャーの説明)
- Trunk公式ドキュメント(設定リファレンス、アセットパイプラインの説明)
- wasm-bindgen公式ガイド(Wasm向けRustの基礎)
- Binaryenプロジェクト(wasm-optの使い方)
- console_error_panic_hook クレートドキュメント(crates.io)

