Game state
Before we add any more functionality to our game it’s time for some
refactoring. To make it easier to keep track of the game state we’ll add an
enum called GameState
with variants to differentiate between the game being
played and the game being over. This will allows us to remove the gameover
variable, and we can add states for showing a start menu and pausing the game.
Implementation
Game state enum
Begin by adding an enum called GameState
below the Shape
implementation.
It should contain all four possible game states: MainMenu
, Playing
,
Paused
, and GameOver
.
enum GameState {
MainMenu,
Playing,
Paused,
GameOver,
}
Game state variable
Replace the line that declares the gameover
variable with a line that
instantiates a game_state
variable set to GameState::MainMenu
.
let mut game_state = GameState::MainMenu;
Match on GameState
We’ll replace the old code in the game loop with code that uses the match
control flow construct on the game_state
variable. It has to match on all
four states in the enum. Later on we’ll add back code from the earlier chapter
within the matching arms. Keep the call to clearing the screen at the start of
the loop, and the call to next_frame().await
at the end.
clear_background(DARKPURPLE);
match game_state {
GameState::MainMenu => {
...
}
GameState::Playing => {
...
}
GameState::Paused => {
...
}
GameState::GameOver => {
...
}
}
next_frame().await
Main menu
Now let’s add back code into the match arms to handle each game state. When
the game is started, the state will be GameState::MainMenu
. We’ll start by
quitting the game if the Escape
key is pressed. If the player presses the
space key we’ll set the game_state
to the new state GameState::Playing
.
We’ll also reset all the game variables. We will also draw the text “Press
space” in the middle of the screen.
GameState::MainMenu => {
if is_key_pressed(KeyCode::Escape) {
std::process::exit(0);
}
if is_key_pressed(KeyCode::Space) {
squares.clear();
bullets.clear();
circle.x = screen_width() / 2.0;
circle.y = screen_height() / 2.0;
score = 0;
game_state = GameState::Playing;
}
let text = "Press space";
let text_dimensions = measure_text(text, None, 50, 1.0);
draw_text(
text,
screen_width() / 2.0 - text_dimensions.width / 2.0,
screen_height() / 2.0,
50.0,
WHITE,
);
},
Playing the game
Let’s add back the code for playing the game to the matching arm for the state
GameState::Playing
. It’s the same code as most of the game loop from the
last chapter. However, don’t add back the code that handles Game Over as it
will be added in the matching arm for the GameState::GameOver
.
We’ll also add a code that checks if the player presses the Escape
key and
change the state to GameState::Paused
. This will ensure that the game will
be paused in the next iteration of the game loop.
GameState::Playing => {
let delta_time = get_frame_time();
if is_key_down(KeyCode::Right) {
circle.x += MOVEMENT_SPEED * delta_time;
}
if is_key_down(KeyCode::Left) {
circle.x -= MOVEMENT_SPEED * delta_time;
}
if is_key_down(KeyCode::Down) {
circle.y += MOVEMENT_SPEED * delta_time;
}
if is_key_down(KeyCode::Up) {
circle.y -= MOVEMENT_SPEED * delta_time;
}
if is_key_pressed(KeyCode::Space) {
bullets.push(Shape {
x: circle.x,
y: circle.y,
speed: circle.speed * 2.0,
size: 5.0,
collided: false,
});
}
if is_key_pressed(KeyCode::Escape) {
game_state = GameState::Paused;
}
// Clamp X and Y to be within the screen
circle.x = clamp(circle.x, 0.0, screen_width());
circle.y = clamp(circle.y, 0.0, screen_height());
// Generate a new square
if rand::gen_range(0, 99) >= 95 {
let size = rand::gen_range(16.0, 64.0);
squares.push(Shape {
size,
speed: rand::gen_range(50.0, 150.0),
x: rand::gen_range(size / 2.0, screen_width() - size / 2.0),
y: -size,
collided: false,
});
}
// Movement
for square in &mut squares {
square.y += square.speed * delta_time;
}
for bullet in &mut bullets {
bullet.y -= bullet.speed * delta_time;
}
// Remove shapes outside of screen
squares.retain(|square| square.y < screen_height() + square.size);
bullets.retain(|bullet| bullet.y > 0.0 - bullet.size / 2.0);
// Remove collided shapes
squares.retain(|square| !square.collided);
bullets.retain(|bullet| !bullet.collided);
// Check for collisions
if squares.iter().any(|square| circle.collides_with(square)) {
if score == high_score {
fs::write("highscore.dat", high_score.to_string()).ok();
}
game_state = GameState::GameOver;
}
for square in squares.iter_mut() {
for bullet in bullets.iter_mut() {
if bullet.collides_with(square) {
bullet.collided = true;
square.collided = true;
score += square.size.round() as u32;
high_score = high_score.max(score);
}
}
}
// Draw everything
for bullet in &bullets {
draw_circle(bullet.x, bullet.y, bullet.size / 2.0, RED);
}
draw_circle(circle.x, circle.y, circle.size / 2.0, YELLOW);
for square in &squares {
draw_rectangle(
square.x - square.size / 2.0,
square.y - square.size / 2.0,
square.size,
square.size,
GREEN,
);
}
draw_text(
format!("Score: {}", score).as_str(),
10.0,
35.0,
25.0,
WHITE,
);
let highscore_text = format!("High score: {}", high_score);
let text_dimensions = measure_text(highscore_text.as_str(), None, 25, 1.0);
draw_text(
highscore_text.as_str(),
screen_width() - text_dimensions.width - 10.0,
35.0,
25.0,
WHITE,
);
},
Pause the game
Many games have the option to pause the action, so we’ll add support for that
in our game, too. When the game is paused, we’ll check if the player presses
the Space
key and change the game state to GameState::Playing
so that the
game can continue. We’ll also draw a text on the screen showing that the game
is paused.
The changed game state will only come into effect in the next iteration of the game loop, so even if it has been changed we need to display the text during the current frame.
GameState::Paused => {
if is_key_pressed(KeyCode::Space) {
game_state = GameState::Playing;
}
let text = "Paused";
let text_dimensions = measure_text(text, None, 50, 1.0);
draw_text(
text,
screen_width() / 2.0 - text_dimensions.width / 2.0,
screen_height() / 2.0,
50.0,
WHITE,
);
},
Game Over
Finally we will handle what happens when the game is over. If the player
presses the space bar we’ll change the state to GameState::MainMenu
to allow
the player to start a new game or quit the game. We’ll also draw the “GAME
OVER!” text to the screen as we did in the last chapter.
GameState::GameOver => {
if is_key_pressed(KeyCode::Space) {
game_state = GameState::MainMenu;
}
let text = "GAME OVER!";
let text_dimensions = measure_text(text, None, 50, 1.0);
draw_text(
text,
screen_width() / 2.0 - text_dimensions.width / 2.0,
screen_height() / 2.0,
50.0,
RED,
);
},
Since the states for GameState::Playing
and GameState::GameOver
are
separated, the squares and circles will not be shown when the game is paused.