Rust & Wasmでミニカートゲームを作る
はじめに
今回は、Cnavas上で動作するカートゲームを作りました。昔、初めて"Basic Magazime"を読んでタイピングして遊んだのが、本ミニゲームのような、障害物を避けながらゴールを目指すようなゲームでした。GWに、当時の記事見たさに、青梅市にある”マイコン博物館"に行ってみたのですが、残念ながら、当時の記事を見つけることはできませんでした😢。画面のイメージは、障害物が"#"で、"●"のヘビのように下から上にスクロールして...というような感じのゲームだったと思います。
今回も、興味を持って頂いた方は、パソコンからお試しください🙂。
ミニカートゲーム

お試しURL
操作
カーソル左/右: 左右移動
カーソル上: 速度UP
カーソル下: 左右の移動をなくし、機体をまっすぐにします。
スペース: ゲーム開始/再開
スペース(プレー中):ブレーキ
※今回はSP版に対応できていません。理由は、"カーソル下"の操作をどうやってTouchで表現すれば良いのか思いつかなかったため。
ソースコード
ステートマシンの実装について
ゲームの流れは、最初にオープニング画面が表示されます。スタートすると、画面中の車が、カーソルからの入力に従い移動し、障害物の壁も車の速度に合わせて画面の上から下に向けて移動します。車がゴールまで辿り付くとゲームクリアーの文字が表示され、途中、障害物の壁と交差すると、車が故障したイメージに変わり移動が停止し、ゲームオーバーの文字が表示されます。この後、"スペース"を押すと、再度、ゲームが始まります。今回は、これらのゲームの流れを、関数の中で、STATUSをmatch文で分岐するのではなく、「RustとWebAssemblyによるゲーム開発※」で紹介されている"ステートマシン"を使った実装にしています。以下の表が車の状態遷移表ですが、transition()関数に集約され実装しています。TypeScriptなどの他の言語でも同様な実装は、もちろんできます。ただ、rustのmatchが"タプルマッチング: (state, event)のような複数値の同時マッチングができる"ことと、From/Intoトレイトの簡潔さが、rustにおいて、強力にステートマシン実装を推していると思います。
| NO | 遷移前の状態 | イベント | 遷移後の状態 | 処理 |
|---|---|---|---|---|
| 01 | Idle | Update | Update | スタンディング |
| 02 | Idle | Run(veloity) | Running | スタート |
| 03 | Running | Update | Running | 走行中 |
| 04 | Running | Run(veloity) | Running | 加速中 |
| 05 | Running | Knocked | Knocked | クラッシュ |
そのまま実装できます。
fn transition(self, event: Event) -> Self {
match (self.clone(), event) {
(CartStateMachine::Idle(state), Event::Update) => state.update().into(),
(CartStateMachine::Idle(state), Event::Run(velocity)) => state.run(velocity).into(),
(CartStateMachine::Running(state), Event::Update) => state.update().into(),
(CartStateMachine::Running(state), Event::Run(velocity)) => {
state.run(velocity).into()
}
(CartStateMachine::Running(state), Event::Knocked) => state.knocked().into(),
_ => self,
}
}
これまでWasmから呼び出したことがあるブラウザAPIと使い方
Wasmからは、直接ブラウザーのAPIにアクセスできないため、wasm-bindgen + web_sys を仲介して、ファイルの読込や、イベント、外部APIへのアクセスを実現します。ただその実装方法は、直接呼び出せるものもあれば、Closureから呼び出さなければならない場合もあり、使いたいAPIによって異なります。いざ実装する時に、毎回、調べるのも少し大変なので、
実際のAPI一覧 ここで、例えばファイルを読み込む関数の"fetch_with_str"を検索窓に入力すれば、使い方はわかりますが、Rustで組み込んでみてと言われると...戸惑います。
そこで、これまで呼び出したAPIだけでも、実装時に参考になるようにまとめておきたいと思います。今時は、このような一覧も不要かもと思いますが...。
| 項目 | 内容 |
|---|---|
| NO | 01 |
| 目的 | ゲームループ作成 |
| 関数名 | browser::request_animation_frame |
| 分類 | イベントリスナー |
| 特徴 | 自身を再帰的に呼び出してGameLoopを実現 |
| 実装例 | https://github.com/myurioka/cart/blob/main/src/wasm/src/engine.rs#L118-L130 |
| NO | 02 |
| 目的 | キーイベント |
| 関数名 | web_sys::KeyboardEvent |
| 分類 | イベントリスナー |
| 特徴 | forget()を呼び出し、関数のスコープを抜けた後でも、イベントを受け取れるようにする |
| 実装例 | https://github.com/myurioka/cart/blob/main/src/wasm/src/engine.rs#L188-L198 |
| NO | 03 |
| 目的 | ファイル(wave,mp3,png...)読込 |
| 関数名 | browser::fetch_array_buffer |
| 分類 | 非同期処理結果待ち |
| 特徴 | 非同期処理が完了するまで、await()を呼び出しコードの実行を一時停止させることが必要 |
| 実装例 | https://github.com/myurioka/cart/blob/main/src/wasm/src/engine.rs#L225-L232 |
| NO | 04 |
| 目的 | マウスイベント |
| 関数名 | web_sys::MouseEvent |
| 分類 | イベントリスナー |
| 特徴 | forget()を呼び出し、関数のスコープを抜けた後でも、イベントを受け取れるようにする |
| 実装例 | https://github.com/myurioka/barragerust/blob/main/src/wasm/src/lib.rs#L384-L394 |
| NO | 05 |
| 目的 | マイク入力 |
| 関数名 | web_sys::SpeechRecognitionResultList |
| 分類 | イベントリスナー |
| 特徴 | マイクから入力されたデータを文字列にしてinput_textにセットする |
| 実装例 | https://github.com/myurioka/adventure/blob/main/src/wasm/src/lib.rs#L86-L142 |
| NO | 06 |
| 目的 | 外部API呼び出し |
| 関数名 | web_sys::XmlHttpRequest |
| 分類 | イベントリスナー |
| 特徴 | ブラウザーAPIを通して外部APIにPOSTし、レスポンスを受信するとWasm側の受信処理をコールバックする |
| 実装例 | https://github.com/myurioka/adventure/blob/main/src/wasm/src/lib.rs#L231-L318 |
最後に
昔は、雑誌に掲載されたプログラムを書き写していました。その中でも、機械語で書かれたプログラムをタイピングするのは大変だっと記憶があります。時が経ち、今では、自動でコーディングされる時代になりました。プログラミングの楽しさもだいぶ変わってきました。
今回も、興味を持って頂いた方は、パソコンからお試しください😄。
Discussion