Snake the game is basically done at this point. We just have one last feature to add: Keeping score.
We’re going to add current score tracking and high scores, as well as tracking the time in a particular run.
The UI for this is going to be some text on either side of the game board tracking the current score on the left and the high score on the right.
The Score module
Our first work is going to be in the scoring.rs
submodule. We need to add the module to lib.rs
and create the scoring.rs
file.
pub mod assets;
pub mod board;
pub mod colors;
pub mod controls;
pub mod food;
pub mod scoring;
pub mod settings;
pub mod snake;
pub mod ui;
The scoring module will have its own plugin: ScorePlugin
.
ScorePlugin
will initialize three new resources for us: Timer
, Score
, and HighScore
.
The Timer
will track how long the game has been played so far, while Score
and HighScore
will track the current game score and the highest all time score respectively.
When we enter and exit the GameState::Playing
state, we will start and close the timer. We’re using iyes_loopless again for these run conditions.
use std::time::{Duration, Instant};
use bevy::prelude::{App, Plugin, Res, ResMut};
use iyes_loopless::prelude::AppLooplessStateExt;
use crate::GameState;
pub struct ScorePlugin;
impl Plugin for ScorePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<Timer>()
.init_resource::<Score>()
.init_resource::<HighScore>()
.add_enter_system(
&GameState::Playing,
start_timer,
)
.add_exit_system(
&GameState::Playing,
close_timer,
);
}
}
The Score
struct will track the number of apples eaten, so it holds a u32.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Score {
pub score: u32,
}
The HighScore
will be set when the game ends, if the current score is better than the last one.
The HighScore
also includes a Duration
, which represents how much time passed for the run.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct HighScore {
pub score: u32,
pub time: Duration,
}
The Timer
has two fields: start
and runtime
.
start
is set to an Instant
when the game starts, marking the time at which the game started.
When the game ends, we also set runtime
using the elapsed
function on the start
Instant
. This returns the Duration
from the Instant
to when the game ended.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Timer {
pub start: Option<Instant>,
pub runtime: Option<Duration>,
}
impl Default for Timer {
fn default() -> Self {
Timer {
start: None,
runtime: None,
}
}
}
start_timer
sets the Timer
's start
field to Instant::now()
to mark the start of the game and also resets the runtime
field to None
.
While close_timer
, gets the duration, sets it as the Timer
's runtime
, then sets the HighScore
resource if the current run is a better run.
fn start_timer(mut timer: ResMut<Timer>) {
*timer = Timer {
start: Some(Instant::now()),
runtime: None,
};
}
fn close_timer(
mut timer: ResMut<Timer>,
score: Res<Score>,
mut high_score: ResMut<HighScore>,
) {
let elapsed = timer.start.unwrap().elapsed();
timer.runtime = Some(elapsed);
if score.score > high_score.score
|| score.score == high_score.score
&& elapsed < high_score.time
{
*high_score = HighScore {
score: score.score,
time: elapsed,
}
}
}
Altogether these handle all of the infrastructure we need to support keeping score in our game.
Add the plugin to main.rs
. Note that because we’ve used iyes_loopless
run conditions based on the GameState
to determine when the timer should run, we do have to add the ScorePlugin
after that state gets inserted into the app so that the state is available to be checked.
.add_loopless_state(STARTING_GAME_STATE)
.add_plugin(ScorePlugin)
For this whole thing to work we do actually have to increment the score somewhere, so in the same place we detect whether the snake eats an apple, increment the score.
First make sure a ResMut
for Score
is added to tick
in lib.rs
.
pub fn tick(
...
mut score: ResMut<Score>,
) {
Then increment the score by 1 after we send the NewFoodEvent
.
food_events.send(NewFoodEvent);
score.score += 1;
Functionally the only logic we need to add is to reset the score each round. In reset_game
add a ResMut<Score>
and set it to the default value, which is 0.
pub fn reset_game(
mut commands: Commands,
mut snake: ResMut<Snake>,
positions: Query<Entity, With<Position>>,
mut last_pressed: ResMut<controls::Direction>,
mut food_events: EventWriter<NewFoodEvent>,
mut score: ResMut<Score>,
) {
for entity in positions.iter() {
commands.entity(entity).despawn_recursive();
}
food_events.send(NewFoodEvent);
*snake = Default::default();
*last_pressed = Default::default();
*score = Default::default();
}
At this point you could drop a dbg!(&score)
into the close_timer
system and cargo run
to see the score updating.