Pipe movements

Build a Flappy Bird Clone in Rust and Bevy 0.14Part 6 - Pipe movements

Game
Bevy 0.14
Flappy Bird
Last update:

In this part of the tutorial, we're going to do two things:

  • First we're going to write the system for the pipes to move from the right side of the screen to the left side at a constant speed
  • Then, we're going to detect collisions between the bird and the pipes in the same system and set the GameState as GameOver if a collision is detected.

System: Moving the Pipes

We need to create a new Bevy system to move the pipes from the right to the left at a constant speed. Let's create a new system in the systems.rs file and call it pipes:

// systems.rs
 
pub fn pipes(){}

We need the pipes to move at a constant speed based on the time, so we'll need to bring in the Time from the arguments, we also need to query the upper and lower pipes.

// systems.rs
 
pub fn pipes(
    time: Res<Time>,
    mut upper_pipe_query: Query<(&UpperPipe, &mut Transform)>,
    mut lower_pipe_query: Query<(&LowerPipe, &mut Transform), Without<UpperPipe>>,
) {
    let delta = time.delta().as_secs_f32();
    let delta_x = 150. * delta; // The change of x position per refresh
 
    for (_, mut transform) in upper_pipe_query.iter_mut() {
        transform.translation.x -= delta_x;
    }
 
    for (_, mut transform) in lower_pipe_query.iter_mut() {
        transform.translation.x -= delta_x;
    }
}

This will make the pipes to move constantly, but when they go out of screen, they go out forever. We need to reset the pipes to the right side of the screen when they go out of the screen. We can do this by checking the x position of the pipes and resetting them if they go out of the screen.

Learn Rust by Practice

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

Resetting the Pipes

We need to first find the utmost right pipe's position. We can do this by querying the pipes and finding the pipe with the highest x position. After we find that position, we'll reset the pipe that is out of screen to that position + 200.0 which 200.0 is the distance between the pipes.

let utmost_right_pipe = upper_pipe_query // can be lower, doesn't matter
    .iter() // make it an iterator so that we can run `max_by` on it
    .max_by(|(_, a), (_, b)| a.translation.x.partial_cmp(&b.translation.x).unwrap())
    .unwrap()
    .1 // to get the transform and not `UpperPipe`
    .translation
    .x; // x position of the rightmost pipe
  1. Query Conversion: upper_pipe_query.iter() converts the query results into an iterator, enabling the use of iterator methods like max_by.

  2. Finding the Maximum: .max_by(|(_, a), (_, b)| a.translation.x.partial_cmp(&b.translation.x).unwrap()) iterates through the pipes, comparing their x positions to find the pipe with the highest x value.

  3. Unwrapping the Result: .unwrap() ensures that we safely extract the result of the max_by method. This is safe because we assume there is at least one pipe in the query.

  4. Accessing the Transform: .1 accesses the second element of the tuple, which is the transform component containing the position data.

  5. Getting the x Position: .translation.x retrieves the x position of the rightmost pipe, which will be used to reset any pipe that moves out of the screen.

After we found out the rightmost pipe, we can reset the pipes that are out of the screen:

pub fn pipes(
    time: Res<Time>,
    mut upper_pipe_query: Query<(&UpperPipe, &mut Transform)>,
    mut lower_pipe_query: Query<(&LowerPipe, &mut Transform), Without<UpperPipe>>,
) {
    let delta = time.delta().as_secs_f32();
    let delta_x = 150. * delta;
 
    let utmost_right_pipe = upper_pipe_query // can be lower, doesn't matter
        .iter() // make it an iterator so that we can run `max_by` on it
        .max_by(|(_, a), (_, b)| a.translation.x.partial_cmp(&b.translation.x).unwrap())
        .unwrap()
        .1 // to get the transform and not `UpperPipe`
        .translation
        .x; // x position of the rightmost pipe
 
    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 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;
        }
    }
 
    for (_, mut transform) in lower_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 = lower_y;
        }
    }
}

We can check if the pipe is out of screen by checking if the x position of the pipe is less than -(WINDOW_WIDTH / 2.) - 26.. The 26. is the half of the width of the pipe. If the pipe is out of the screen, we reset the pipe to the right side of the screen.

We then re-use the random_pipe_position function to get the new y position of the pipes, and reset the position once the pipe is out of the screen.

Collision checking

Now that we have the pipes moving, we need to check for collisions between the bird and the pipes. We can do this by checking if the bird's position is inside the pipe's position. To do that we can check if the bird's x position is between the pipe's x position and the pipe's x position + the pipe's width and the bird's y position is between the pipe's y position and the pipe's y position + the pipe's height.

Since this logic goes beyond the scope of learning Rust and Bevy, we'll not explain the code in detail. You can read the code and try to understand it if you want.

let is_collision = |bird_transform: &Transform, pipe_transform: &Transform| -> bool {
    let bird_x = bird_transform.translation.x;
    let bird_y = bird_transform.translation.y;
    let bird_width = 34.0;
    let bird_height = 24.0;
 
    let pipe_x = pipe_transform.translation.x;
    let pipe_y = pipe_transform.translation.y;
    let pipe_width = 52.0;
    let pipe_height = 320.0;
 
    let collision_x = bird_x + bird_width / 2.0 > pipe_x - pipe_width / 2.0
        && bird_x - bird_width / 2.0 < pipe_x + pipe_width / 2.0;
    let collision_y = bird_y + bird_height / 2.0 > pipe_y - pipe_height / 2.0
        && bird_y - bird_height / 2.0 < pipe_y + pipe_height / 2.0;
 
    collision_x && collision_y
};

Here, we defined a closure is_collision that takes the reference of the bird's and pipe's Transform and returns a boolean value. The closure calculates the positions and dimensions of the bird and the pipe, then checks if the bird is colliding with the pipe based on the conditions provided.

If true, that means the collision happened and we'll reset the game.

pub fn pipes(
    time: Res<Time>,
    mut upper_pipe_query: Query<(&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 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;
        }
    }
 
    for (_, mut transform) in lower_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 = lower_y;
        }
    }
 
    let is_collision = |bird_transform: &Transform, pipe_transform: &Transform| -> bool {
        let bird_x = bird_transform.translation.x;
        let bird_y = bird_transform.translation.y;
        let bird_width = 34.0;
        let bird_height = 24.0;
 
        let pipe_x = pipe_transform.translation.x;
        let pipe_y = pipe_transform.translation.y;
        let pipe_width = 52.0;
        let pipe_height = 320.0;
 
        let collision_x = bird_x + bird_width / 2.0 > pipe_x - pipe_width / 2.0
            && bird_x - bird_width / 2.0 < pipe_x + pipe_width / 2.0;
        let collision_y = bird_y + bird_height / 2.0 > pipe_y - pipe_height / 2.0
            && bird_y - bird_height / 2.0 < pipe_y + pipe_height / 2.0;
 
        collision_x && collision_y
    };
 
    for bird_transform in bird_query.iter_mut() {
        let mut game_over = || {
            game.state = GameState::GameOver;
            *game_over_query.single_mut() = Visibility::Visible;
 
            // Play game over sound
            commands.spawn(AudioBundle {
                source: asset_server.load("audio/hit.ogg"),
                settings: PlaybackSettings::DESPAWN,
            });
        };
 
        for (_, transform) in upper_pipe_query.iter_mut() {
            if is_collision(bird_transform, &transform) {
                game_over();
            }
        }
 
        for (_, transform) in lower_pipe_query.iter_mut() {
            if is_collision(bird_transform, &transform) {
                game_over();
            }
        }
    }
}

As you can see, there is nothing new we used here, we used the commands to play the hit sound and set the GameState to GameOver if a collision is detected.

Let's run the code and see if the pipes move and the collision detection works.

Pipes moving and collision detection Pipes moving and collision detection


Now, pipe movements and collision detection are implemented. In the next 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. See you in the next part!