Scoring

Build a Flappy Bird Clone in Rust and Bevy 0.14Part 7 - Scoring

Game
Bevy 0.14
Flappy Bird
Last update:

In the previous part, we implemented the pipe movements and collision detection. In this part, we'll implement the score counting logic and update the score state when the bird passes the pipes. We'll also render the score on the top-right corner of the screen.

Updating the score

In order to update the score, we need a way to know that if a pipe is in the left side of the bird or in the right side, we also need to know that if the pipe has already been counted so that we don't count it again and only increase the score by 1 for each pipe.

For that, we'll need to store a piece of data for each pipe to know if it has been passed by the bird or not. To do that, we first need to add the new field to the pipes, which is going to be a boolean.

Learn Rust by Practice

Master Rust through hands-on coding exercises and real-world examples.

Updating components

We only need to update one of the pipes, either the upper pipes or the lower pipes, for this tutorial, we're going to only update the UpperPipe struct.

// components.rs
 
#[derive(Component)]
pub struct UpperPipe {
    pub passed: bool,
}

Updating setup

Since we added a new field, we also need to change the code that spawns the pipes to include the new field, the default value of the new field is going to be false.

// setup.rs
  ...
  // Spawn Upper Pipe
  commands.spawn((
      SpriteBundle {
          texture: asset_server.load("texture/pipe.png"),
          transform,
          ..default()
      },
      UpperPipe { passed: false },
  ));

There's also a few more modifications we need to do to the code:

  • Reset passed of all pipes to false when the game restarts, we want to reset the pipes to their initial position and also reset the passed field to false so that the score can be counted again.
  • Reset passed of the pipes to false when they reset their position to the right side of the screen.
  • Add a score counting system that will increment the score by 1 whenever the bird passes a pipe.

Resetting pipes when the game restarts

When the game restarts, we need to reset the pipes to their initial position and also reset the passed field to false.

We can do that in the start_game system that we previously created.

pub fn start_game(
    mut game: ResMut<Game>,
    mut space_query: Query<(&mut PressSpaceBarText, &mut Visibility)>,
    mut game_over_query: Query<&mut Visibility, (With<GameOverText>, Without<PressSpaceBarText>)>,
    mut bird_query: Query<(&mut Bird, &mut Transform)>,
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut upper_pipe_query: Query<(&mut Transform, &mut UpperPipe), (With<UpperPipe>, Without<Bird>)>,
    mut lower_pipe_query: Query<
        &mut Transform,
        (With<LowerPipe>, Without<Bird>, Without<UpperPipe>),
    >,
) {
    if !keyboard_input.just_pressed(KeyCode::Space) {
        return;
    }
 
    if game.state == GameState::GameOver {
        for (i, (mut transform, mut upper_pipe)) in upper_pipe_query.iter_mut().enumerate() {
            let delta_x = i as f32 * 200.0 + 200.;
 
            upper_pipe.passed = false;
            transform.translation.x = 0.;
            transform.translation.x += delta_x;
        }
 
        for (i, mut transform) in lower_pipe_query.iter_mut().enumerate() {
            let delta_x = i as f32 * 200.0 + 200.;
 
            transform.translation.x = 0.;
            transform.translation.x += delta_x;
        }
    };
    ...
}

We changed the upper_pipe_query to also query for the UpperPipe component and get a mutable reference of it, this is because we're going to reset the passed field of all the pipes to false when the game restarts.

Resetting passed pipes

When the pipes reset their position to the right side of the screen, we also need to reset the passed field to false, we can update the pipes system that we created before.

pub fn pipes(
    time: Res<Time>,
    mut upper_pipe_query: Query<(&mut UpperPipe, &mut Transform)>,
    mut lower_pipe_query: Query<(&LowerPipe, &mut Transform), Without<UpperPipe>>,
    mut bird_query: Query<&Transform, (With<Bird>, Without<LowerPipe>, Without<UpperPipe>)>,
    mut game_over_query: Query<&mut Visibility, With<GameOverText>>,
    asset_server: Res<AssetServer>,
    mut game: ResMut<Game>,
    mut commands: Commands,
) {
    let delta = time.delta().as_secs_f32();
    let delta_x = 150. * delta;
 
    let utmost_right_pipe = upper_pipe_query
        .iter()
        .max_by(|(_, a), (_, b)| a.translation.x.partial_cmp(&b.translation.x).unwrap())
        .unwrap()
        .1
        .translation
        .x;
 
    let new_pipe_position = utmost_right_pipe + 200.0;
    let (lower_y, upper_y) = random_pipe_position();
    let out_of_screen_x = (-WINDOW_WIDTH / 2.) - 26.;
 
    for (mut upper_pipe, mut transform) in upper_pipe_query.iter_mut() {
        transform.translation.x -= delta_x;
 
        if transform.translation.x < out_of_screen_x {
            transform.translation.x = new_pipe_position;
            transform.translation.y = upper_y;
            upper_pipe.passed = false;
        }
    }
    ...
}

Now whenever the pipes position resets, the passed field also resets to false.

Score counting system

Now that we have made the necessary changes to the pipes, we can now implement the score counting system. The logic is simple, whenever we detect a pipe that is in the left side of the bird and the passed field is false, we increment the score by 1 and mark the pipe as passed by setting the passed field to true.

// systems.rs
 
pub fn score(
    mut game: ResMut<Game>,
    bird_query: Query<(&Bird, &Transform)>,
    mut upper_pipe_query: Query<(&mut UpperPipe, &Transform)>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    for (_, bird_transform) in bird_query.iter() {
        for (mut upper_pipe, transform) in upper_pipe_query.iter_mut() {
            let passed = transform.translation.x < bird_transform.translation.x;
            let passed_state = upper_pipe.passed;
 
            if passed && !passed_state {
                game.score += 1;
                upper_pipe.passed = true;
 
                commands.spawn(AudioBundle {
                    source: asset_server.load("audio/point.ogg"),
                    settings: PlaybackSettings::DESPAWN,
                });
 
                println!("Score: {}", game.score);
            }
        }
    }
}

Updating the Bevy App:

// main.rs
 
fn main() {
    App::new()
        .init_resource::<Game>()
        .add_systems(Startup, setup)
        .add_systems(Update, blink_space_bar_text.run_if(is_game_not_active))
        .add_systems(Update, move_background.run_if(is_game_active))
        .add_systems(Update, move_ground.run_if(is_game_active))
        .add_systems(Update, animate_bird.run_if(is_game_active))
        .add_systems(Update, start_game.run_if(is_game_not_active))
        .add_systems(Update, gravity.run_if(is_game_active))
        .add_systems(Update, jump.run_if(is_game_active))
        .add_systems(Update, pipes.run_if(is_game_active))
        .add_systems(Update, score.run_if(is_game_active))
        .add_plugins(MyPlugin)
        .run();
}

This prints the score to the console, let's try it out and see if it works as expected.

Score counting Score counting

Great! The score is now being counted whenever the bird passes the pipes. Now it's time to render the score on the screen.

Rendering the score

In the spawning game entities part, we learned how to use the texture atlas layout to render the numbers on the screen, but we didn't add the logic for it to render the numbers based on the score of the player.

Render Score System

Since we have access of the score in the Game resource, we can create another system and use that score to render the numbers on the screen using the texture atlas layout.

pub fn render_score(game: Res<Game>, mut query: Query<&mut TextureAtlas, With<ScoreText>>) {
    let score_string = format!("{:03}", game.score); // Ensure at least 3 digits, pad with zeros
    let score_digits: Vec<usize> = score_string
        .chars()
        .map(|c| c.to_digit(10).unwrap() as usize)
        .collect();
 
    for (digit, mut texture_atlas) in score_digits.iter().zip(query.iter_mut()) {
        texture_atlas.index = *digit;
    }
}

The code above formats the score to ensure it has at least 3 digits, then it converts each digit to a number and updates the index field of the TextureAtlas component, since every index of the texture atlas layout corresponds to the actual number, this will render the score on the screen.

Let's try it out!

Rendering the score Rendering the score


That's it! We are successfully finished with building the flappy bird clone in Rust, we hope you enjoyed this tutorial series and learned a lot from it. If you have any questions or feedback, feel free to reach out to us, we'll be happy to help you out. Thank you for reading!