社内ではRustのエッジ製品への適用が本格化し、接続するデバイスに応じたプラグインのデバイスコネクタやSDK等への広がりを見せています。
個人的にもRustでのゲーム開発についての話題を追いかけているのですが、最近は bevy というゲームエンジンに勢いがあるようです。このbevyはWebAssemblyにビルドし、ブラウザ上で動作させることにも対応しています。というわけで、bevyで作ったアプリケーションをブラウザ上で動作させてみます。
bevyとは
bevyはECSに基づいたRust製のゲームエンジンで、本体はシンプルに保ちつつ、プラグインを導入して拡張を容易にする設計になってます。bevy用のプラグインは有志によって多数開発されているようです。
bevy自体は頻繁にアップデートが続けられているため、サードパーティ製のプラグインも更新されなければ使えなくなってしまう心配がありますが、有用なものはやはり使っていきたいものです。Rustで人気のあるGUIライブラリであるeguiをbevyで使用できるようにしたbevy_eguiもあり、今回はこれでGUIを作ってみます。
実装
Cargo.toml
を以下のように記述します。bevyは2022年11月時点で最新のバージョンです。
[package] name = "bevy-test" version = "0.1.0" edition = "2021" [dependencies] bevy = "0.9" bevy_asset_loader = "0.14" bevy_egui = "0.17" rand = "0.8"
main.rs
は以下のように記述します。
use bevy::prelude::*; use bevy_asset_loader::prelude::*; use bevy_egui::{egui, EguiContext, EguiPlugin}; use rand::Rng; fn main() { App::new() .add_loading_state( LoadingState::new(GameState::AssetLoading) .continue_to_state(GameState::Running) .with_collection::<MyAssets>(), ) .add_state(GameState::AssetLoading) .add_system_set(SystemSet::on_update(GameState::Running).with_system(ui_system)) .add_system_set(SystemSet::on_enter(GameState::Running).with_system(setup)) .add_plugins(DefaultPlugins.set(WindowPlugin { window: WindowDescriptor { title: "bevy test".into(), width: 320.0, height: 320.0, ..Default::default() }, ..default() })) .add_plugin(EguiPlugin) .run(); } #[derive(Resource, AssetCollection)] struct MyAssets { #[asset(path = "rust-logo.png")] img: Handle<Image>, } fn setup(mut commands: Commands) { let camera = Camera2dBundle::default(); commands.spawn(camera); } fn ui_system( mut commands: Commands, mut egui_context: ResMut<EguiContext>, my_assets: Res<MyAssets>, mut tex_entities: Local<Vec<Entity>>, ) { egui::Window::new("test").show(egui_context.ctx_mut(), |ui| { if ui.button("add").clicked() { let mut rng = rand::thread_rng(); let id = commands .spawn(SpriteBundle { texture: my_assets.img.clone(), transform: Transform::from_xyz( rng.gen_range(-160.0..160.0), rng.gen_range(-160.0..160.0), 0.0, ), ..default() }) .id(); tex_entities.push(id); } if ui.button("clear").clicked() { for id in tex_entities.iter() { commands.entity(*id).despawn(); } tex_entities.clear(); } }); } #[derive(Clone, Eq, PartialEq, Debug, Hash)] enum GameState { AssetLoading, Running, }
少し解説を入れます。
App::new() .add_loading_state( LoadingState::new(GameState::AssetLoading) .continue_to_state(GameState::Running) .with_collection::<MyAssets>(), ) .add_state(GameState::AssetLoading) .add_system_set(SystemSet::on_update(GameState::Running).with_system(ui_system)) .add_system_set(SystemSet::on_enter(GameState::Running).with_system(setup))
bevy単体だとアセットの扱いが結構苦行なので、bevy_asset_loaderを使っています。MyAssets
のロードが完了するとGameState
がAssetLoading
からRunning
に移行します。Running
に移行したときにSystemSet::on_enter
で指定したシステムsetup
が動作します。ui_system
はRunning
状態でしか呼ばれないこともここで指定しています。
#[derive(Resource, AssetCollection)] struct MyAssets { #[asset(path = "rust-logo.png")] img: Handle<Image>, }
AssetCollection
をderive
で指定して、ロードしたいアセットを定義します。ここでは、rust-logo.png
というファイルがロードされるようにします。
fn ui_system( mut commands: Commands, mut egui_context: ResMut<EguiContext>, my_assets: Res<MyAssets>, mut tex_entities: Local<Vec<Entity>>, ) { egui::Window::new("test").show(egui_context.ctx_mut(), |ui| { if ui.button("add").clicked() { let mut rng = rand::thread_rng(); let id = commands .spawn(SpriteBundle { texture: my_assets.img.clone(), transform: Transform::from_xyz( rng.gen_range(-160.0..160.0), rng.gen_range(-160.0..160.0), 0.0, ), ..default() }) .id(); tex_entities.push(id); } if ui.button("clear").clicked() { for id in tex_entities.iter() { commands.entity(*id).despawn(); } tex_entities.clear(); } }); }
ui_system
はGUIを定義するシステムで、eguiで生成したウィンドウ上にボタンを2つ用意します。addボタンはクリックされたとき、MyAssets
で読み込んだテクスチャを乱数で決めた位置に貼り付けます。このときspawn
したエンティティのIDを記録しておき、clearボタンが押されたときに消去するようにします。
ビルド
Unofficial Bevy Cheat Bookにはwasm向けビルド方法がいくつか紹介されていますが、今回はwasm-bindgenを用います。
wasm-bindgenは以下でインストールします。
cargo install wasm-bindgen-cli
以下のコマンドでビルドできます。
cargo build --release --target wasm32-unknown-unknown wasm-bindgen --target web --out-dir . --no-typescript target/wasm32-unknown-unknown/release/bevy-test.wasm
ビルドが成功すると、bevy-test_bg.wasm
とbevy-test.js
という2つのファイルができるので、それを呼び出すindex.html
ファイルを用意します。
<html> <head> <meta charset="utf-8"/> </head> <script type="module"> import init from './bevy-test.js' init() </script> </html>
あとは適当なHTTPサーバーを用意し、
python3 -m http.server 8000
ブラウザで開けば動作確認できます。addボタンを押せば画像がランダムに配置され、clearボタンを押せばそれが消えるのを確認できるはずです。
まとめ
今回はRust+bevyで簡単なアプリケーションをwasm向けにビルド、動作確認してみました。eguiによりGUIも比較的簡単に記述することができ、ビルド自体も非常に簡単に行えました。Rustでさくっと書いたものがwebブラウザ上で動くのはなかなか感慨深いものがあります。まだまだRustでのゲーム開発は発展途上であり、bevyもまた開発中ではありますが、webブラウザで動かしたい場合にbevyは有力な選択肢になるのではないでしょうか。