RustとBevyでゲームを作ってみた

最近スマートバンドを買いました。ストレスとか睡眠の質が数値で出るのがなんかとても楽しいです。

つい最近ゲームが作りたくなったので、思い切ってRustでゲームを作ってみました。

なんのゲーム?

ボンバーマンとスプラトゥーンを足して2で割ったような感じのゲームです。

  • ルール
    • 爆弾の爆発を相手に巻き込ませて、相手を倒す
    • 自分の残機が0にならず相手の残機を0にしたら勝ち
      • ゲーム開始・終了のエフェクトは入れてないです
    • 爆弾が爆発すると自分のインクが飛び散る
    • キャラクターが自分のインクの上を移動する場合は高速に移動できる、反対に、相手のインクの上を移動する場合は低速になってしまう
    • プレイヤーが移動する方向に視界があり、敵キャラクターが視界外にいると画面から見えない
  • 操作方法
    • ASDW: 前後左右に移動
    • スペース: 爆弾配置
      • 爆弾は最大5つまでしか同時に置けない

どうやって作れる?

RustのBevyというツールを利用してゲームを作りました。

Bevyは、Unityなどと同様のゲーム制作ツールですが、GUIが一切ありません。
カメラや敵の配置、スピードなど全てRustのコードで再現する必要があります。

Bevyでゲームを作るには エンティティーコンポーネントシステム(ECS)という設計パターンに沿ってコードを書く必要がありました。

ECSって?

雑にいうと、エンティティが同一性を表すID、コンポーネントが同一性に付与される色々なデータ、システムがそれらを扱うトランザクションスクリプト、という感じでしょうか。 IDとそれに紐づくデータを厳密に分けて管理しているのでオブジェクト指向とは扱い方が異なりその点がとても面白く感じました。

なんでエンティティとコンポーネントが分離されているかというと、処理効率を上げるためのようです。
というのも、ゲームでは処理効率が重要視されており、処理が止まったりフレームレートが落ちたりしないようにメモリの配置が工夫されています。

従来のオブジェクト指向では、IDとデータが1つのクラスの中に詰め込まれ、これはメモリには連続した範囲に配置されます。

例えばオブジェクトのクラスに速度(Velocity)と位置(Position)のデータを保つとします。その場合はこのような順序でメモリに配置されます

  • オブジェクトAのID
  • オブジェクトAのVelocityデータ
  • オブジェクトAのPositionデータ
  • オブジェクトBのID
  • オブジェクトBのVelocityデータ
  • オブジェクトBのPositionデータ

しかし、ゲーム開発ではさまざまなオブジェクトの特定のデータのみに連続してアクセスしたい場合が多いようです。上記の例で言うと「Positionデータだけにアクセスしたい」と言うような具合ですね。

オブジェクトAとBのPositionデータにアクセスするためには以下のように非連続な範囲にアクセスする必要があります。 このようにオブジェクト指向ではアクセスするべきメモリの場所がバラバラになってしまうので、アクセス効率が悪いです。

  • オブジェクトAのID
  • オブジェクトAのVelocityデータ
  • オブジェクトAのPositionデータ
  • オブジェクトBのID
  • オブジェクトBのVelocityデータ
  • オブジェクトBのPositionデータ

この問題をECSは解決しています。上記のようなデータをECSで管理する場合はメモリの配置が以下のようになります。

結果としてデータへのアクセス効率が良くなり処理効率が上がる、と言う仕組みです。

  • オブジェクトAのID(=エンティティ)
  • オブジェクトBのID(=エンティティ)
  • オブジェクトAのVelocityデータ(=コンポーネント)
  • オブジェクトBのVelocityデータ(=コンポーネント)
  • オブジェクトAのPositionデータ(=コンポーネント)
  • オブジェクトBのPositionデータ(=コンポーネント)

また、ECSはこのほかにもオブジェクト指向でみられる深い継承関係を防ぎ、保守性を高く保てると言うメリットもあります。

Rustはそもそもオブジェクト指向を採用していないのでECSと相性が良さそうですし、オブジェクト指向で無理やり作った時の可変参照・不変参照の解決順序を考えなくて良いのでECSでのゲーム開発に適してるように感じました。

面白かった?

めちゃくちゃ面白かったです、普段作るWebアプリケーションやネイティブアプリと違う考え方が色々なところに見られて刺激的でした!
というのも、普段はオブジェクト指向に寄ったプログラムを書くことが多いので、それとの違いが大きかったためです。

具体的には、保持するデータの大きさの違いがあります。オブジェクト指向であればその概念に紐づくデータを1つのクラスで管理しますが、ECSでは機能ごとにデータをComponentという単位で分割します。その概念が持っているデータがいろいろな箇所に散乱してしまうように感じますが、逆に言うとその機能を別の概念に横展開することも簡単になります。例えば、当たり判定を敵だけでなく壁にも適用したい場合は当たり判定コンポーネントを壁に適用するだけですみます。まさにPHPのtraitを思い出しながら書いていました。

また、Systemはコンポーネントを操作する処理を記載する部分ですが、Bevyでは、利用するコンポーネントをSystemの引数であるQueryに設定する必要があり、同時にそのコンポーネントが不変参照・可変参照であるかを表記する必要があります。この表記があるおかげでBevyがデータの競合が発生せずに並行処理ができるのだろうと感じました。

また、Rustは型を厳密に記述する必要があるため、ゲームの仕様を変えた際にコンパイルエラーで修正すべき部分を気づくことができました。これによって、ゲームを動かして試して少し仕様を変えてまた動かして、というPDCAがとてもやりやすかったです

あとは、自分で作った物がヌルヌル動くことが刺激的でワクワクしながらずっと作業できました!w
今のゲーム開発ではC言語やC++がメジャーだと思いますが、いつの日かRust/Bevyが積極的に採用される日が来ないかなぁ… そうなったらゲームプログラマになりたい

前に同じようにRustでターミナルで動くボンバーマンみたいなゲームを作ったこともありましたが、やっぱりリッチなGUIがあるともっと面白く感じますね

どうやってビルド・デプロイした?

Bevyは、Windows, MacOS, Linux, Webのいずれの環境でも実行できるようになっていて、アプリケーションをwasm化することでWebブラウザでも実行可能なようです。
なのでwasm化させてブログにデプロイさせるだけで十分でした

開発中はずっとMacOSでしか起動確認してなかったのでWebブラウザで動かすには骨が折れるだろうと思っていましたが、 Unofficial Bevy Cheat Book/Browser (WebAssembly)の手順に基本的に従うだけで簡単にできました。 しかし、以下のビルドの手順は自分の環境でうまくいかなかったので注意が必要です

cargo build --release --target wasm32-unknown-unknown
-wasm-bindgen --out-dir ./out/ --target web ./target/
+wasm-bindgen --out-dir ./out/ --target web ./target/wasm32-unknown-unknown/release/mygame.wasm

また、MacOSでの挙動を100%再現できている訳ではありませんでした。ゲームの仕様として視界の外は暗くなるような視覚効果を入れていたのですが、wasmにするとそうなっていませんでした。

/img/2022-10-29/game_5.gif

本当はこんな感じで視界の外は暗くなるはず

また、なぜか敵キャラクターがエリアの外に出てしまいます。原因はわかりませんがRustとjsでの数値計算の互換性がなんかおかしくなって壁抜けできてしまっているのかなと考えてます

/img/2022-10-29/game_6.gif

その他

Bevyの使い方はyoutubeでRust Bevy Full Tutorial - Game Development with Rust - Git Code Updated to Bevy v0.8 !!!を見て写経して学びました、youtubeの中のコードはBevyのバージョンが若干古いですが、数も多くなくその都度ググれば見つかります

ゲームで利用する素材はitch.ioから集めてました、無料で配布していると思えないようなクオリテぃのものばかりだったので本当にありがたいです

ゲームに使うドット絵の一部を改変したいことがありましたが、これにはPixelStudioを使わせていただきました。色の一括置換機能があったおかげでめちゃくちゃ作業が捗りました


See also