私がBevy Engineでかなり重要だと思っている『ステート』について解説していきます。
ステート
Bevyにおけるステート(State)とは、ざっくりいうと「ゲームがいまどの画面を開いているか」、つまり最初のタイトル画面なのか、ゲーム本編が始まっているのか、コンフィグ画面なのか、などを表すものです。
たとえば、タイトル画面でスペースキーを押したらゲーム本編に進むという仕組みだったとしましょう。それでゲーム本編に入ったとき、もう一度スペースキーを押したらまたゲーム本編の初期化の処理が実行されてしまうことになります。頑張ってフラグで切り替えてゲーム本編ではそのシステムが動作しないようにしたとしても、システム自体は動作しているのでクエリが実行されてしまい、ゲーム本編には不要な処理が何度も走ってしまう可能性があります。
ここでステートを使うと、特定の画面でのみ動作するシステムを定義できます。
ステートの定義
次は実際に私が書いたステートの定義です。
#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum GameState {
#[default]
Setup,
/// タイトル画面
MainMenu,
/// 通常のプレイ画面
InGame,
/// ワープ中を表すステート
Warp,
// 名前入力画面
NameInput,
}
ゲームを起動すると、最初はデフォルトのSetup
ステートになります。Setup
では画像などの読み込みを行い、読み込みが完了するとMainMenu
ステートに遷移するようになっています。そしてタイトル画面で画面をクリックするとゲーム本編のInGame
ステートに遷移します。
なお、グローバルな状態を管理する機能という意味では、ステートと似たようなものに『リソース』という機能もあります。リソースとステートの違いは、ステートはシステムの切り替えに使うもので、リソースはそれ以外の汎用的なグローバル状態だということです。
システムの実行条件
先ほど定義したステートを使ってシステムの実行条件をしてするには、run_if
とin_state
を使います。
app.add_systems(
FixedUpdate,
update_camera.run_if(in_state(GameState::InGame))
);
こうすると、update_camera
システムは、ゲームがInGame
ステートにあるときだけ実行されるようになります。実際の開発を始めると、add_systems
のほとんどでこのrun_if
でステートを指定することになります。このようにステートは実はゲーム全体に影響しますので、なるべく開発の初期に定義しておいたほうがいいと思います。
ステートの切り替え
ステートを切り替えるには、Bevy組み込みのNextState
リソースを参照します(リソースについてはこのシリーズでまだちゃんと説明していませんが、汎用のグローバルな状態のことです)。次のようにResMut
システムパラメータを使ってリソースを取り出し、set
で次のステートを指定します。
fn on_click(
...
mut next_state: ResMut<NextState<GameState>>,
) {
...
next_state.set(GameState::InGame);
...
}
これで、この次のフレームからはGameState::InGame
のステートになります。たStateScoped
が動作して、不要になったエンティティが自動的に削除されます。
StateScoped
ステートのもうひとつ重要な機能に、StateScoped
があります。これはコンポーネントの一種なのですが、別のステートに切り替わったときに自動的にそのエンティティ全体をワールドから削除することができます。たとえば、私のゲームではプレイヤーキャラクターに次のようにStateScoped
コンポーネントを追加しています。
commands.spawn((
StateScoped(GameState::InGame),
...
});
これで、タイトル画面に戻ったときにはプレイヤーキャラクターのエンティティは自動的に削除されます。手動でエンティティを削除するのはかなり面倒ですし、うっかり削除し損ねるとタイトル画面に戻ったのにプレイヤーキャラクターのスプライトが画面に残ったりします。そのため実際にはほとんどすべてのエンティティにこのStateScoped
を追加することになります。この意味でもステートは最初に定義しておいたほうがいいということです。