Rust & Wasm でミニ迷路ゲームを作る
はじめに
「RustとWebAssemblyによるゲーム開発」を参考に作成した「3D迷路ゲーム」を紹介させて頂きます。3D迷路ゲームは、「マイコンBASICマガジン」を読んでいる人であれば、一度は、チャレンジしたテーマだと思います。今回、"Rust & Wasm"を実使い、実際に作ってみた感想についても記載させて頂きます。
対象のバージョン
- Rust 2024 Edition
- rustc 1.83.0
- cargo 1.80.1
- rustup 1.27.1
- pnpm 9.12.3
Rust&Wasm ミニ迷路ゲーム
ゴーストに捕まらず、迷路から脱出してください。
脱出するためには、迷路中にある箱を拾いスタート地点に戻って、"スペースキー/中央をタップ"してください。
拾った箱は、ゴーストから隠れることもできます。その時も、"スペースキー/中央をタップ"してください。
操作
PCの場合
スペースキー: スタート,箱をひろう,箱を使う,箱に隠れる,
テンキー「↑」: 前に進む
テンキー「→」: 右に向きをかえる
テンキー「↓」: 後ろに進む
テンキー「←」: 左に向きをかえる
SPの場合
画面中央タッチ: スタート,箱をひろう,箱を使う,箱に隠れる
画面上部タッチ: 前に進む
画面右部タッチ: 右に向きをかえる
画面下部タッチ: 後ろに進む
画面左部タッチ: 左に向きをかえる
RustとWasmについて
昔、Javaで記述されたプログラムを、ブラウザ上で実行される「Javaアプレット」としてHTMLに埋め込むことができましたが、「Javaアプレット」は、埋め込んだプログラムがファイルシステムへのアクセスや、ネットワーク、メモリへのアクセスを制限された"サンドボックス"という環境で実行していました。Wasmも、同じく"サンドボックス"上で実行され、直接外部リソースにアクセスできない仕様です。そのため、ファイル取得、外部HTTP通信は、JavaScriptAPIを通してブラウザのAPIを使用する仕様です。
ファイルや、ネットワークなどの外部リソースへのアクセスは、非同期処理のため、JavaScriptAPIにリクエストした後、Wasmでは、レスポンスを待つ必要があります。その方法として、「WebAssembly JavaScript Promise統合(JSPI)※」という、Wasm側で、JavaScriptAPIにアクセスし、Promiseが返されたときにWasm側を一時停止し、Promiseが解決された時に再開できるようにする仕様を実装しようとしているようです(まだ実装したことがないです😢)。ちなみに、Geminiに、"JSPIは実現されるか?"を質問してみると、「実現までの見のりは不確実な要素もあるが、実現する可能性は高い」と教えてくれました。
※Introducing the WebAssembly JavaScript Promise Integration API(JSPI)
このような状況なので、外部リソースからコールバック処理は、「RustとWebAssemblyによるゲーム開発」でも紹介されていますが、コールバック先をforget()関数を使ってWasmからの参照を解放することで、Javascript側でコールバックできるようになります👍。最初は、とても違和感がありましたが、各JavaScriptAPIとは、ほとんど同じパターンで実装できます。今は、非同期処理は、JSPIよりも現在の方法の方が、しっくりきます。
The Typestate Pattern in Rust
ゲーム中の状態管理を、「RustとWebAssemblyによるゲーム開発」](https://www.oreilly.co.jp/books/9784814400393/)で紹介されている「Typestate Pattern」を使って作りました。そのパターンの使い方は、ゲーム中の動作(移動中,攻撃中,ジャンプ中...)やシーン(オープニング、進行中、エンディング)の状態遷移を型で定義し、それぞれの状態遷移を、型中に定義されたメソッドを使って遷移するように作る感じです。"Typestate Pattern"も、WasmのJS操作と同じように、慣れまで時間がかかりましたが、作っている内に、既にあるステータスに、"動作"や"シーン"を追加することは容易であることがわかり、このパターンを使い続けるようになると思いました。
「RustとWebAssemblyによるゲーム開発」で紹介されている次のドキュメントがわかりやすいと思います。
The Embedded Rust Book
The Typestate Pattern in Rust
3D迷路について
迷路の表示については、1次元配列で表現されたマップ情報(0:通路 1:壁 2:出口 3:敵 8:箱)をもとに、draw関数を使って、壁や敵などを表示しています。奥行は、
図中の番号順に、壁の境界線を描画することで表現しています。

また、向きの回転は、方角に合わせて、マップ情報から取得する配列のインデックスを縦横を切り替え(⇒例)ています。
例:
北を向いている場合は、 目の前のインデックス: 現在インデックス - 列のマス数
東を向いている場合は、 目の前のインデックス: 現在インデックス + 1
南を向いている場合は、 目の前のインデックス: 現在インデックス + 列のマス数
西を向いている場合は、 目の前のインデックス: 現在インデックス - 1

Discussion