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.