Bevy allows us to use Rust enums to build high-level state machines that control which systems are running. Typically these are used for creating behavior like pause menus. Our version of 2048 doesn't really benefit from a pause menu, so we'll be using these states to implement "Game Over".
First we can declare a RunState
enum. Enums are types whose value can be one of a set of variants. In this case we have a RunState
enum whose value can be Playing
or GameOver
.
enum RunState {
Playing,
GameOver,
}
In our App::build
chain, we'll add the state to our game. This creates a new state machine with the drivers and extra functionality that makes it all work.
.add_state(RunState::Playing)
If we cargo run
now we'll see a number of errors related to RunState
implementing various traits.
error[E0277]: `RunState` doesn't implement `Debug`
--> src/main.rs:206:20
|
206 | .add_state(RunState::Playing)
| ^^^^^^^^^^^^^^^^^ `RunState` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `RunState`
= note: add `#[derive(Debug)]` or manually implement `Debug`
To fix this we can derive the trait implementations for the constraints Bevy has required.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
PartialEq
is the only trait that wasn't included in the list of errors. This is because deriving Eq
requires deriving PartialEq
. The short version of why is that Eq
doesn't actually add any new methods, it only exists to inform the compiler that there is a "full equivalence" as opposed to a "partial equivalence" for the type. This means that Eq
is probably what you were expecting when you derived Eq
and PartialEq
is the version of equality you might be used to in JavaScript, where NaN != NaN and such things.
Now that we've derived all the traits we need for our type, we can start controlling which systems run in which RunState
. To do this we'll add a SystemSet
to our App::build
.
There are a number of hooks we can use to run systems at different times, such as when we enter a state, exit a state, or in the update stage. We'll use this to add all of the gameplay systems to the update stage of our new state machine.
We'll remove the old systems and only use the ones we added to the new SystemSet.
.add_system_set(
SystemSet::on_update(RunState::Playing)
.with_system(render_tile_points.system())
.with_system(board_shift.system())
.with_system(render_tiles.system())
.with_system(new_tile_handler.system())
.with_system(end_game.system()),
)
If we run the game now, we should see no differences in behavior.
To end the game we can add a query for the State<RunState>
resource to the end_game
system.
fn end_game(
tiles: Query<(&Position, &Points)>,
query_board: Query<&Board>,
mut run_state: ResMut<State<RunState>>,
) {
This lets us set the RunState
to GameOver
when we detect that there are no more moves.
if has_move == false {
dbg!("game over!");
run_state.set(RunState::GameOver).unwrap();
}
Setting the state to RunState::GameOver
will cause all of the systems we added to the RunState::Playing
state to stop running. We can see this happen because we no longer see "game over!"
spamming the console.