はじめに
Tiny Gladeというゲームが9月末にSteamで販売開始されました。
この記事を書いている時点でレビューが12,000件以上ついているという盛況ぶりです。こちらのゲームでは Bevy Engine というゲームエンジンが使われているそうです1。
今回のアドベントカレンダーは、ちょっとしたゲームを作りながらこのBevy Engineの解説記事で完走を目指して走っていきたいと思います。もっとも、とてもすべては解説しきれないので、BevyやECSの大まかな概念やTipsなどに絞っていく予定です。
ちなみに2022年のアドベントカレンダーで書いたRustのまほう1期はこちらです。
Bevy Engineについて
Bevyは次のような特徴のゲームエンジンです。
- プログラミング言語Rustのライブラリ
- 2Dと3Dの両方に対応
- クロスプラットフォーム (Windows, MacOS, Linux, Web, iOS, Android)
- オープンソース
- Entity Component System (ECS)というアーキテクチャ
Bevyはまだまだ開発途上の段階にあり、バージョンアップのたびにゴリゴリAPIが変わっているようです。なにしろ「まだAPIが不安定だしRustでゲーム作るならGodotとかを検討してもいいんじゃない?」と公式が自分で言っているくらいです。あくまでゲームを完成させることが目的の人は、素直に Unity や Unreal Engine、あるいはオープンソースが好みなら Godot Engine などを使ったほうが安心できると思います。でも自分は今回、以下のような理由で Bevy にしました。
- プログラミング言語Rustを練習がてら何か作ってみたかった
- オブジェクト指向を脱却したデータドリブンなECSアーキテクチャを体験してみたかった
- UnityやUnreal Engineはすでに幾らでも資料や記事があるので、私が書くことはもうあまりない。日本語圏で資料が少ないもののほうが執筆のやる気が出る
- 多機能なGUIエディタつきのゲームエンジンより、シンプルなライブラリのほうが性に合っている気がする
Bevyはまだ若いエンジンのわりにはコミュニティが比較的大きく、ユーザーの熱意も感じます。探してみると良さげなプラグイン(ライブラリ)もたくさん見つかります。本体の開発も活発で、このあいだBevy財団が発足して寄付を募っているなどコアチームの開発体制づくりにも力が入っています。もしこの記事の読者のなかに大富豪がいらっしゃいましたら、ぜひとも寄付してもらえればコミュニティ全員が喜ぶと思います。
自分が作っているゲームについて
今回私が作っているのは、ジャンルでいうと「ツインスティックシューター」などと呼ばれている種類のものです。これは文字通り「左のスティックと右のスティック(またはWASDとマウス)でそれぞれ移動と攻撃の方向を指示」という操作の2Dのアクションゲームです。
私がプレイしたことのあるゲームでいうと「Enter the Gungeon」が典型的で、「Factorio」「Noita」のようなゲームも操作が同様です。FPSのような3次元的なゲームより操作が簡単でわかりやすく、私はこのジャンルのゲームが好きなのでこれにしました。
現状だとどうにか基本的なシステムは形になってきていて、プレイヤーキャラクターと敵キャラクターがなんとなくそれっぽく戦えたり、オンラインでプレイヤー同士が簡単なバトルをできるというところです。
詳しい内容については、今後の記事に少しづつ書いていきたいと思います。
Bevyで画像を表示してみる
このシリーズはあくまでプログラミングの記事なので、早速ですがウィンドウを開いて何か画像を表示するコードを実際に書いて、Bevyの雰囲気を体験してみましょう。まずはいつものように cargo init
でプロジェクトを初期化し、bevy
クレートを依存関係に追加します。
$ cargo init
$ cargo add bevy
ウィンドウを開いてスプライト(画像)を表示する最低限のコードは以下のようになります2。
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
commands.spawn(SpriteBundle {
texture: asset_server.load("bevy_bird_dark.png"),
..default()
});
}
それからassets
フォルダ以下に画像や音声などのアセットを置きます。assets
がBevyのデフォルトのアセットパスです。あとは cargo run
で実行すれば、ウィンドウが開いて画像が表示されるはずです。
main関数
少し詳しくコードの中身を見てみます。
まずは app.add_plugins(DefaultPlugins)
でデフォルトのプラグインを設定しています。Bevyの個々の機能は、 プラグイン として個別に提供されます。ウィンドウを表示する機能やスプライトを表示する機能などの最小限の機能をまとめたのがDefaultPlugins
で、これを登録することで最低限ウィンドウを開いて画像を表示することができます3。
次の app.add_systems(Startup, setup)
では、Startup
というスケジュール で setup
システム を登録しています。Bevyにおけるシステムとは、特定のパターンの引数を持った関数のことです。またスケジュールとはそのシステムが呼び出されるタイミングのことです。要するに、イベントにイベントハンドラを登録しているようなものだと考えればいいでしょう4。
最後に app.run()
を呼び出して実行を開始します。
setup関数
Startup
はゲームの開始時に1度だけ実行されるスケジュールなので、先ほど登録された setup
システムが一度だけ実行されます。まずCamera2dBundle::default()
でカメラを作成しcommands.spawn
でゲーム世界に追加しています。ゲーム画面を表示するには、少なくともひとつのカメラが必要です。
次に asset_server.load("bevy_bird_dark.png")
で画像のパスを指定し、画像のハンドルを取得しています5。そしてそれを SpriteBundle
に渡して初期化し、commands.spawn
で追加しています。
そしてBevy 0.15へ……
実はこの記事が投稿される前日に、Bevy 0.15 が正式リリース されました。この変更により、上記のコード中のCamera2dBundle
やSpriteBundle
が非推奨になりました。0.15対応のコードだと以下のようになります。
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d::default());
commands.spawn(Sprite::from_image(asset_server.load("bevy_bird_dark.png")));
}
自分も必死にリリースノートを読んでいるところです。Required Componentsという機能が追加されたのですが、これは「Bevyが始まって以来最も重大なAPI改善」だといいます。確かに今までのAPIよりもシンプルに書けるようになって、良い感じだとは思います。破壊的変更は恐ろしいですが、Bevy界隈は活発なので外部のBevyプラグインもすぐに追従してきてくれることでしょう。
次回予告
これだけならあまりECSっぽさもないので、とくに難しくないですね。次回以降は、Bevyの最大の特徴である Entity Component System について解説したり、Bevyでの開発について実践的な部分の解説をできたらと思います。
-
ただしレンダリングはBevyのものではなく、Vulkan上で独自のレンダリングパイプラインが実装されているそうです。作者インタビュー→ https://80.lv/articles/exclusive-tiny-glade-developers-discuss-bevy-proceduralism-publishers-cozy-games/ ↩
-
これはバージョン 0.14 のコードなのですが、実は次のバージョン 0.15 で書き方が変わりそうです。変化が激しい……。 ↩
-
サーバーサイドで動かすなど、ヘッドレスに実行したい場合はこの
DefaultPlugin
を登録しない場合もあるようです。 ↩ -
ただしBevyの機能としての「イベント」はまた別に存在します。 ↩
-
勘のいい人は「ファイルアクセスなのに同期的でいいのか?」と思うかもしれませんが、この
asset_server.load
は画像データそのものではなくハンドルを即座に返します。あとはエンジンが実際の読み込みを裏で行い、完了しだい画面に描画してくれるので、開発者がデータの非同期読み込みを意識する必要がなくなっています。 ↩