youtu.be今日は、なんか疲れてて、下の動画が結構難しくて2周くらいしてたら昼過ぎに寝落ちしちゃって、そのあと2時くらいから5時くらいまで活動した。でも、夕飯食べてお風呂入って、散歩してから、8時11時で3時間も集中した。なので、昼寝を取り返す活躍が出来た気がする。
youtu.be今回もやっていきましょうね。
目的
今回の目的は、ゲームの状態遷移を実装し、ボタン押下によってメインメニューとゲームとゲームオーバーの状況を変更できるようにします。
また、メインメニューは以下の要素を持ちます。
・ボタンを押すと、ゲームに移行できる
・画面には何も表示しない。
・デフォルトである。
ゲームは以下の要素を持ちます。
・特定のボタンを押すと、ポーズと再開ができる
・ポーズをすると、エネミー、スターの生成が止まり、 プレイヤー、エネミーの移動が出来なくなる。
・前回まででプログラミングした星集めゲームを遊べる。
・二度目のゲームを開始するとハイスコアがリセットされる
・敵にヒットするとゲームオーバーになる。
・複数回、ゲームステートに移行するとそのたびに、過去のスコアを保存できる。
ゲームオーバーは以下の要素を持ちます。
・ゲームオーバーになると、全てのエンティティが消去される。
・メインメニューに移行できる。
目的への取り組み
まずは、ソースフォルダーに、gameと、main_menuという新しいフォルダーを作成し、それらに、mod.rsファイルを新規作成した。
その後、gameフォルダーに既存のenemy, player, score, starフォルダーをgameフォルダー内に移動した。
また、gameフォルダーの中に、systems.rsというファイルを作った。

このような感じである。
その後、game.rsにおいて、EnemyPlugin, ScorePlugin, StarPlugin, PlayerPluginをまとめたGamePluginというプラグインを作り、また、それを前々回作ったmain.rsファイルのようにmod, useの組み合わせで参照を整え、各ファイル内の絶対パスが変わりエラーが出た部分をgame::を加えスコープに入れなおした。
現在のmod.rsは以下のとおりである。
また、main.rsには、gameをスコープに入れ、メイン関数に、GamePluginに入れた、4つのプラグインを削除し、GamePluginを加えた。
また、main_menuのmod.rsファイルには、仮のMainMenuPluginを作成した。
こちらも、同様にして、main.rsに追加した。
現在問題なく動いている。
ここで、ゲームのポーズ機能を実装するために、ステートを用いる。
Stateはゲームの流れを管理するためのもので、ポーズ機能、設定、等に使えると非公式bevy説明書に書いてある。
States allow you to structure the runtime "flow" of your app.
This is how you can implement things like:
- A menu screen or a loading screen
- Pausing / unpausing the game
- Different game modes
- …
In every state, you can have different systems running. You can also add setup and cleanup systems to run when entering or exiting a state.
bevy-cheatbook.github.io
まず、Stateの宣言は以下の様に行った。
ポーズをデフォルト状態にしたいので、Pausedの上に、#[default]と記載した。
これが無いと、#[derive(States, Debug, ..., Default)]にエラーが出るから注意である。
これの意味は、SimulationStateにはRunningと、Pausedという二つの状態があるよ。また、最初はPausedだよという事である。
次に、GamePluginにそれを追加した。
// Specify the initial value:
app.insert_state(MyAppState::LoadingScreen);// Or use the default (if the type impls Default):
app.init_state::<MyGameModeState>();
app.init_state::<MyPausedState>();
bevy-cheatbook.github.iodefaultがある場合の追加は、init_state::<>();を使えばいいらしい。
これにより、SimulationStateを追加できた。
また、ステートを変化させるシステムも作成した
crate::game::systemsの中に、以下の様に作成した。
fn toggle_pause_game(
state: Res<State<MyPausedState>>,
mut next_state: ResMut<NextState<MyPausedState>>,
) {
match state.get() {
MyPausedState::Paused => next_state.set(MyPausedState::Running),
MyPausedState::Running => next_state.set(MyPausedState::Paused),
}
}
bevy-cheatbook.github.ioこの非公式説明書を見ると、トグルの形でのステートの変更はこのように、matchを使うことでうまくいくと書いてありますね。 matchはすべての初期条件を記述しないとエラーが出るので注意。
また、現在のステートを取得する、ステートを変更するためには別々の引数を取らないといけないようです。 simulation_state: Res<State<SimulationState>>,
mut next_state: ResMut<NextState<SimulationState>>,これらがそれにあたります。
現在のステートを取得するには、simulation_state.get()を用い、変更するには、next_state.set(StateName::State)を使う必要があります。
動画では、commands: Commandを使って、ステートを変更させていましたが、コピペしてもエラーが沢山出てよくわかりませんでした。多分bevy 0.15.0では使えないのかもしれません。
私が記述したコードは、引用した例に加えてButtonInputリソースにより、押下されたボタンによって場合分けを追加しました。これにより、スペースキーを押すたびに、PausedとRunningが交互に変更されるシステムを書くことが出来ました。
そして、このしすてむをまた、GamePluginに追加しました。
このように追加したのちに、main.rsを起動してみた。
その際に、スペースを何度か押した。

you are ont the main menuは後で記述するとして、
5行目のコードからわかるよう、ボタンを押した際にRunningと表示され、ステートが切り替わったことが分かる。そのあと、何度か押してもRunningとPausedが交互に表示されていて、問題なく遷移できていることが分かる。
これにより、実際のエンティティの生成や動きを止める機能はまだ実装できていないものの、RunningとPausedという二つのステートを作成することが出来た。
次に、同様にして、ゲーム全体にも
Game, GameOver, MainMenuというステートを作成していこうと思う。
SimulationStateと同じように、宣言した。MainMenuがデフォルトである。
先ほど同じように今度は、メイン関数にAppStateに追加した。
また、簡単なMainMenuPluginというものを作っておいた、
それら二つをメイン関数に追加した
これにより、先ほどの実行結果に you are on the main menuが表示されていた。
目的にある、
メインメニュー
・ボタンを押すと、ゲームに移行する
ゲーム
・星に当たると、ゲームオーバーに移行する
・ボタンを押すと、メインメニューに移行する
ゲームオーバー
・ボタンを押すと、メインメニューに移行する。
これを実装していこうと思う
crate::systemsに以下の様なシステムを記述した。
これにより、メインメニューからJを押すと、ゲームステートに移動して、
ゲームオーバーとゲームステートから、Mを押すと、メインメニューステートに移行するシステムを作りました。
それぞれ移行したら、それをテキストで表示するようになっています。
次に、ちょうどゲームオーバーイベントといういいものがあったので、既存の関数に追加する形で記述した。
これも、星にヒットするとゲームから、ゲームオーバーに移行するようにした。
この場合は表示する文字列は必要ないと感じたので書かなかった。
そして、上の新しい二つの関数を、Updateスケジュールでメイン関数に追記した。

それをコンパイルしたのち、mmmjjjmmmjjjエスケープキーという風に入力したのが以上の実行結果である。
実行結果は、実行結果が表示されている部分の7行目でGame stateに移行した表示が初めて登場していて、その前には、Main Stateに移行したという表示はない。つまり、アップステートのデフォルトはメインメニューになっていることと、メインメニューでmを押下しても何も処理が実行されていないことをさす。また、ゲームに移行した後2回jが押下されているが、表示は一回である。そのため、これは適切にtransition_to_game_stateシステムが動いていることを表す。また、8行目には、メインメニューに移行したことが表示されている。また、それは一回であるので、transition_to_main_menu_stateシステムも適切に動いている。
これで、ステートの遷移は実装することが出来た。
なので、次にやることは現在のステートに応じてシステムを実行できるようにすることだ。
実装すべき機能は以下の通り、
- メインメニューではウィンドウを表示するだけ。
- ゲームに入ったら、エネミー、スター、プレイヤーをスポーンさせる。
- ポーズを解除すれば、エネミー、プレイヤーの移動、スター、エネミーの新規スポーンが発生する。
- ゲームオーバーになれば、全てのエンティティを削除する。
まずは、ゲームになったらと、一回機能する関数について書く。 まずは、
app.add_systems(OnEnter(MyAppState::LoadingScreen), (
start_load_assets,
spawn_progress_bar,
));
引用したように、add_systemsの際に、OnEnterというスケジュールを用いれば、その時だけに使用できるとのこと。
これをOnEnter(AppState::Game)にして、スポーンプレイヤー、スポーンエネミー、そしてスポーンスターシステムに適応する。各プラグインのStartUpスケジュールになってたシステムのスケジュールをすべてOnEnter(AppState::Game)にした。

ゲーム開始後入力をせずに約五秒経過した後の実行結果である。
デフォルトではメインメニューなので、ゲームステートに入らずに、スターとエネミーの時間経過生成だけが働いている。
次に、
- ポーズを解除すれば、エネミー、プレイヤーの移動、スター、エネミーの新規スポーンが発生する。
この機能を実装するために、
app.configure_sets(FixedUpdate, (
// configure the same set here, so we can use it in both
// FixedUpdate and Update
MyGameplaySet
.run_if(in_state(MyAppState::InGame))
.run_if(in_state(MyPausedState::Running)),
// configure a bunch of different sets only for FixedUpdate
MySingleplayerSet
// inherit configuration from MyGameplaySet and add extras
.in_set(MyGameplaySet)
.run_if(in_state(MyGameModeState::Singleplayer)),
MyMultiplayerSet
.in_set(MyGameplaySet)
.run_if(in_state(MyGameModeState::Multiplayer)),
));
この部分を参考にして、.run_if(in_state(StateName::State))という部分が使えると考えた。
実際にはゲームでポーズ解除した時に動くという条件を設定したい場合は、
.run_if(in_state(AppState::Game)).run_if(in_state(SimulationState::Running))が使えると考えた。
変更を加えた部分は、プラグインの部分である。
以上の様に各プラグインで、ヒットに関するシステム、動作に関するシステム、自動発生に関するシステムに、 .run_if(in_state(AppState::Game))
.run_if(in_state(SimulationState::Running))の条件を加える変更を加えた。
すると実行結果では、

起動後操作をせず5秒以上待った後の画面である。
これにより、メインメニューステートでは何も動作していないことが分かった。

次に、jキーを押下した直後の実行結果が以上の画像である。この時、エネミーやスターの追加は無く、矢印キーを入力してもプレイヤーは動かないし、エネミーも動いていないポーズ状態では、それらのシステムが実行されていないことが分かる。また、プレイヤー、スター、エネミーが発生しており、OnExit(AppState::Game)のスケジュールでの関数が正しく動いていることが分かった。

スペースキーを押下してwasdによりいくらか移動した時の実行結果が上の画像である。
この時、プレイヤーの移動エネミーの移動、エネミーとスターの発生が確認できたこれにより、.run_if(.in_state(SimulationState::Running))の条件が上手く機能していることが分かる。
また、エネミーがヒットして、ゲームオーバーになった際、スター、エネミーの発生、エネミーの移動が止まった。これにより、.run_if(in_state(AppState::Game))の条件が正しく機能していることが分かる。
次に、ゲームオーバーに移行した時に全てのエンティティを削除するという仕様を実装する。
ステートのもうひとつ重要な機能に、StateScopedがあります。これはコンポーネントの一種なのですが、別のステートに切り替わったときに自動的にそのエンティティ全体をワールドから削除することができます。たとえば、私のゲームではプレイヤーキャラクターに次のようにStateScopedコンポーネントを追加しています。
(中略)手動でエンティティを削除するのはかなり面倒ですし、うっかり削除し損ねるとタイトル画面に戻ったのにプレイヤーキャラクターのスプライトが画面に残ったりします。そのため実際にはほとんどすべてのエンティティにこのStateScopedを追加することになります。この意味でもステートは最初に定義しておいたほうがいいということです。
今回は、生成したエンティティは、ゲームステートにしか存在する必要が無いので、この機能を使ってみましょう。
fn spawn_player(mut commands: Commands) {
commands.spawn*1;
}
具体的には上のコードの様に、エンティティをcommands.spawnのコマンドを使いスポーンさせるときに、StateScoped(StateName::State)と打ち込むと、ただ、そのコンポーネントが追加されます。
app.init_state::<GameState>();
app.enable_state_scoped_entities::<GameState>();
また、アプリで以上の宣言をすることで、特定のステートから離れるとStateScoped(StateName::State)の付いたエンティティが自動的に削除されるようになります。
これの理解が全然できていなくて、実装に苦労した。
頑張ってStateScopedを入れても、引用した2行が無ければ、機能しない。それはさっきのBevyの日本語のサイトには載っていなくて、こういうところでつまずくんだよな。と思った。
今回の場合spawn_と名の付くシステム内にあるcommands.spawnの全てに、StateScoped(AppState::Game),をエンティティとして宣言したのちに、メイン関数にstatescopedを追加すればいいです。そうすれば、ゲーム以外では消去されます。
全て同じなので、game::enemy::systemsで変更したところだけを、記載します。
このように、ただ、commands.spawn内に、StateScoped(AppState::Game)を書いただけです。crate::game::player::systems, crate::game::star::systemsも同様に書き加えました。
また、メイン関数を
以上の様に修正しました。
.enable_state_scoped_entities::<AppState>()を追加しないと、せっかく書いても機能しません。
実行結果は

エネミーがヒットした時の実行結果が以上の画像です。これでゲームで作られた、エンティティが、ゲームオーバーになったら消滅したことが分かり、StateScopedが機能していることが分かりました。
最後にまだ、機能として、ゲームオーバーになった時に、ポーズすることと、Scoreを0に戻すことが細かい事としてあります。何故なら、ゲームオーバーになった後、メインメニューに戻り、ゲームに移行すると、初めから、エネミーが動いてきて、リスキルされるからです。また、scoreを0に戻さないと、ハイスコアの意味がなくなってしまうからです。
まず、ゲームオーバーになった時に、ポーズする機能を追加します。
上記の様にhandle_game_over関数に、追加で.set(SimulationState::Paused)を使い、ゲームオーバーした時に、SimulationStateが、ポーズになるように設定した。

以上が、jスペースを押下しゲームを開始しヒットされた後に、mjを押下した場面のスクリーンショットです。この場合、もし、ラニング状態であれば、エネミーがヒットしています。そのため、この場面では、ポーズ状態であることが分かります。また、この場面では、いくら待ってもエネミーが増えたり、動いたりすることはありませんでした。
これにより、ゲームオーバーすると、ポーズになる機能が実装されました。
最後に、スコアを更新するシステムです。
現状スコアプラグインは以上のようになっており、最初の一度にしか値が更新されません。その為、.init_resource::<Score>()をゲームに入る度に更新しなければなりません。それをこのコマンドのままでやるのは嫌なので、score::systemsに以下のようなシステムを追加しました。
そして、score::modで、以下の様にOnEnter(AppState::Game)で呼び出しました。
それで実行するとエラーがでたので、Scoreに依存する
このシステムもゲームでしか動かないようにしました。
最終的に
このようにして実行しました。
しかし、スコア表示は上記の様に、どんどん足されました。そのため、スコアのリソースを一度削除する関数も追加することにしました。
このようにOnExitスケジュールで、Scoreリソースを削除する関数を追加しました。
OnExitは、OnEnterとほとんど同じで、スケジュールの一種で、AppState::Gameから離れたときに起動します。

上は三回メインメニュー、ゲーム(wasdキーを使ってスターを取るようにプレイする)、ゲームオーバーを繰り返して実行結果です。
ターミナルの一番下の行HighScoreで、2回目は即死したのが0点として表示されています。つまり、これはゲームに入る度にスコアの初期化に成功しております。
そのため求めていた機能の実装に成功です。
評価
今回の目的は、ゲームの状態遷移を実装し、ボタン押下によってメインメニューとゲームとゲームオーバーの状況を変更できるようにします。
また、メインメニューは以下の要素を持ちます。
・ボタンを押すと、ゲームに移行できる
・画面には何も表示しない。
・デフォルトである。
ゲームは以下の要素を持ちます。
・特定のボタンを押すと、ポーズと再開ができる
・ポーズをすると、エネミー、スターの生成が止まり、 プレイヤー、エネミーの移動が出来なくなる。
・前回まででプログラミングした星集めゲームを遊べる。
・二度目のゲームを開始するとハイスコアがリセットされる
・敵にヒットするとゲームオーバーになる。
・複数回、ゲームステートに移行するとそのたびに、過去のスコアを保存できる。
ゲームオーバーは以下の要素を持ちます。
・ゲームオーバーになると、全てのエンティティが消去される。
・メインメニューに移行できる。
以上の条件は、すべて満たした。
また、細かい機能として、ゲームオーバーになった時に、ポーズすることも実装できました。
よって、完了である。
*3: