Physics

Build a Flappy Bird Clone in Rust and Bevy 0.14Part 5 - Physics

Game
Bevy 0.14
Flappy Bird
Last update:

In the previous part we added a resource Game and stored the GameState and score of the player inside of it.

In this part, we're going to add some physics to the game, mainly the gravity and jumping mechanics. We'll make the bird fall down when the game starts, and when the player presses the space bar key, the bird will jump up.

The game logic

Before we start getting into the physics, it's important for you to understand how we implement the game so that you won't be confused and you know what we're trying to do.

You might be thinking the bird is moving towards the pipes, even though you can take that approach if you want, but we won't do that for this tutorial, instead we make the pipes move to the left side of the screen and once they move out of the screen, we reset their position, again and again.

However, the bird has to move too, but not in the x direction, but instead in the y direction. The bird moves up and down, when the game starts the bird moves down because of gravity, and when the player presses the space bar, the bird moves up.

To achieve this, we not only need to change the bird's position, but we also need to have a property of the bird velocity which is the speed of the bird. Based on the velocity of the bird, we can create a gravity() system, that changes the birds position based on the velocity, if the velocity is positive, the bird moves up, if the velocity is negative, the bird moves down.

Makes sense? Let's get started.

Learn Rust by Practice

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

Gravity System

In order to add gravity to the game, we need to add a new system to the systems.rs file. This system will change the vertical position of the bird, so it looks like the bird is falling down.

But before we do that, let's add a new property to the Bird component to store the bird's velocity.

Updating the Bird Component

Let's add another property to the Bird component to store the bird's velocity.

#[derive(Component)]
pub struct Bird {
    pub timer: Timer,
    pub velocity: f32,
}

Updating setup.rs

...
    Bird {
        timer: Timer::from_seconds(0.2, TimerMode::Repeating),
        velocity: 0.,
    },
...

Adding gravity to the game is just another system in the Bevy App that changes the vertical position of the bird, so it looks like the bird is falling down.

We need to update two things in this system, the y position of the bird, and the speed of the bird, if the bird keeps falling, then it's speed should increase, so that it looks like the bird is falling faster.

We'll add a new system to the systems.rs file to move the bird down.

// systems.rs
 
pub fn gravity(time: Res<Time>, mut query: Query<(&mut Bird, &mut Transform)>) {
    for (mut bird, mut transform) in query.iter_mut() {
        let delta = time.delta().as_secs_f32();
        let gravity = 9.8;
        let delta_v = gravity * 150. * delta;
 
        bird.velocity -= delta_v;
        transform.translation.y += bird.velocity * delta;
    }
}

9.8 is the acceleration due to gravity, it's measured in meters per second squared, we multiply it by 150.0 to make the bird fall faster, you can tweak this number to your liking, we also multiply it by delta which is the time between system updates.

We subtract the delta_v from the bird's velocity, which makes the velocity less and less, so if the velocity hits a negative number, the bird falls down.

We then update the y position of the bird based on the velocity, if the velocity is negative, the bird moves down, if the velocity is positive, the bird moves up.

Add the system to 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_plugins(MyPlugin)
        .run();
}

If we run the game now, the bird will fall down, but but the game will not be over and the bird will keep going, to fix that we need to stop the bird if it touches the ground.

Stopping the bird

To stop the bird when it touches the ground, we need to check if the bird's y position is less than the ground's y position. If it is, then we need to stop the bird and show the game over text.

If you remember the spawning game entities part, we transformed the y position of the Ground by -250.0, so the y position of the ground is -250.0.

To determine the collision of the bird and the ground, we need to find the top part of the ground which is GROUND_Y / 2, and the bottom part of the bird which is BIRD_HEIGHT / 2.

Ground height we can find by examining the texture, which is 112.0px, and bird height is 24.0px. So the collision point can be found by this equation:

let ground_y = -250.0;
let ground_height = 112.0;
let bird_height = 24.0;
 
let collision_point = ground_y + ground_height / 2.0 + bird_height / 2.0;

We'll update the previous gravity system to stop the bird when it touches the ground.

pub fn gravity(time: Res<Time>, mut query: Query<(&mut Bird, &mut Transform)>) {
    for (mut bird, mut transform) in query.iter_mut() {
        let delta = time.delta().as_secs_f32
        let gravity = 9.8;
        let delta_v = gravity * 150. * delta;
 
        bird.velocity -= delta_v;
        transform.translation.y += bird.velocity * delta;
 
        let ground_y = -250.0;
        let ground_height = 112.0;
        let bird_height = 24.0;
 
        let collision_point = ground_y + ground_height / 2.0 + bird_height / 2.0;
 
        if transform.translation.y < collision_point {
            transform.translation.y = collision_point;
            bird.velocity = 0.0;
        }
    }
}

This stops the bird from falling further down when it touches the ground. But the game state won't stop, to do that, we can update the Game resource to GameOver state.

pub fn gravity(
    time: Res<Time>,
    mut game: ResMut<Game>,
    mut query: Query<(&mut Bird, &mut Transform)>,
    mut game_over_query: Query<&mut Visibility, With<GameOverText>>,
) {
    for (mut bird, mut transform) in query.iter_mut() {
        let delta = time.delta().as_secs_f32();
        let gravity = 9.8;
        let delta_v = gravity * 150. * delta;
 
        bird.velocity -= delta_v;
        transform.translation.y += bird.velocity * delta;
 
        let ground_y = -250.0;
        let ground_height = 112.0;
        let bird_height = 24.0;
 
        let collision_point = ground_y + ground_height / 2.0 + bird_height / 2.0;
 
        if transform.translation.y < collision_point {
            transform.translation.y = collision_point;
            bird.velocity = 0.0;
 
            game.state = GameState::GameOver;
            *game_over_query.single_mut() = Visibility::Visible;
        }
    }
}

Let's run the game:

Flappy Bird Gravity Flappy Bird Gravity

Rotating the bird

To make it more lively, we can rotate the bird when it moves up and down. We can do this by querying the Transform component along with the Bird component and rotate the bird based on the bird's velocity in the same gravity system.

pub fn gravity(
    time: Res<Time>,
    mut game: ResMut<Game>,
    mut query: Query<(&mut Bird, &mut Transform)>,
    mut game_over_query: Query<&mut Visibility, With<GameOverText>>,
) {
    for (mut bird, mut transform) in query.iter_mut() {
        let delta = time.delta().as_secs_f32();
        let gravity = 9.8;
        let delta_v = gravity * 150. * delta;
 
        bird.velocity -= delta_v;
        transform.translation.y += bird.velocity * delta;
 
        // Rotate the bird
        let rotation = bird.velocity / 600.0;
        let max_rotation = 0.5;
        transform.rotation = Quat::from_rotation_z(rotation.max(-max_rotation).min(max_rotation));
 
        let ground_y = -250.0;
        let ground_height = 112.0;
        let bird_height = 24.0;
 
        let collision_point = ground_y + ground_height / 2.0 + bird_height / 2.0;
 
        if transform.translation.y < collision_point {
            transform.translation.y = collision_point;
            bird.velocity = 0.0;
            game.state = GameState::GameOver;
 
            *game_over_query.single_mut() = Visibility::Visible;
        }
    }
}

Now the bird will rotate based on the bird's velocity.

Flappy Bird Gravity Rotation Flappy Bird Gravity Rotation

Playing Game over sound

It's nice to hear the game over sound when the collision happens, we can do that by spawning an entity that disappears right away after it finishes playing, so let's bring in the Commands and AssetServer from the arguments to load the sound that we have in the assets/audio/ directory:

pub fn gravity(
    time: Res<Time>,
    mut game: ResMut<Game>,
    mut query: Query<(&mut Bird, &mut Transform)>,
    mut game_over_query: Query<&mut Visibility, With<GameOverText>>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    for (mut bird, mut transform) in query.iter_mut() {
        let delta = time.delta().as_secs_f32();
        let gravity = 9.8;
        let delta_v = gravity * 150. * delta;
 
        bird.velocity -= delta_v;
        transform.translation.y += bird.velocity * delta;
 
        // Rotate the bird
        let rotation = bird.velocity / 600.0;
        let max_rotation = 0.5;
        transform.rotation = Quat::from_rotation_z(rotation.max(-max_rotation).min(max_rotation));
 
        let ground_y = -250.0;
        let ground_height = 112.0;
        let bird_height = 24.0;
 
        let collision_point = ground_y + ground_height / 2.0 + bird_height / 2.0;
 
        if transform.translation.y < collision_point {
            transform.translation.y = collision_point;
            bird.velocity = 0.0;
 
            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,
                ..default()
            });
        }
    }
}

We set the PlaybackSettings to DESPAWN so that the entity will be removed by Bevy right after the sound finishes playing.

Preventing out of bounds

This way if the player keeps pressing the space bar, the bird will go indefinitely up and they will be able to pass all the pipes without trying, this would be a glitch in the game, so we need to prevent the bird from going too far up.

To do that, we'll set a limit of the y position of the bird that it can fly.

pub fn gravity(
    time: Res<Time>,
    mut game: ResMut<Game>,
    mut query: Query<(&mut Bird, &mut Transform)>,
    mut game_over_query: Query<&mut Visibility, With<GameOverText>>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    for (mut bird, mut transform) in query.iter_mut() {
        let delta = time.delta().as_secs_f32();
        let gravity = 9.8;
        let delta_v = gravity * 150. * delta;
        let delta_y = (bird.velocity * delta);
        let new_y = (transform.translation.y + delta_y).min(260.0);
 
        transform.translation.y = new_y;
        bird.velocity -= delta_v;
        ...
    }
}

We are using the min(260.0) method which will compare the two values of the y position of the bird and 260.0, and return the smallest value, so if the bird's y position is greater than 260.0, it will be set to 260.0 so that the bird can't go too far up.

Resetting the bird position

Now, when we press the space bar again, we want the bird position to be reset to the middle of the screen to restart the game, we need to do that in the start_game system that we previously created.

// systems.rs
 
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>>,
) {
    if !keyboard_input.just_pressed(KeyCode::Space) {
        return;
    }
 
    game.state = GameState::Active;
 
    for (mut bird, mut transform) in bird_query.iter_mut() {
        bird.velocity = 0.0;
        transform.translation.y = 0.0;
        transform.rotation = Quat::from_rotation_z(0.0);
    }
 
    let (mut space, mut visibility) = space_query.single_mut();
    space.0.reset();
    *visibility = Visibility::Hidden;
 
    let mut game_over_visibility = game_over_query.single_mut();
    *game_over_visibility = Visibility::Hidden;
}

Now, whenever the game is over and the player presses the space bar again, the bird will be reset to the middle of the screen with velocity of 0.0 and rotation of 0.0.

Jumping mechanic

The jumping mechanic is going to be quite simple, we'll be listening for the space bar key press, and when the player presses the space bar, we'll set the bird's velocity to a positive value, so that the bird moves up.

So, let's create a new system in the systems.rs file:

pub fn jump(
    mut query: Query<&mut Bird>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    keyboard_input: Res<ButtonInput<KeyCode>>,
) {
    if !keyboard_input.just_pressed(KeyCode::Space) {
        return;
    }
 
    commands.spawn(AudioBundle {
        source: asset_server.load("audio/wing.ogg"),
        settings: PlaybackSettings::DESPAWN,
        ..default()
    });
 
    for mut bird in query.iter_mut() {
        bird.velocity = 400.0;
    }
}

The reason this makes the bird move up is that in the gravity system, we wrote this:

let delta_y = bird.velocity * delta;
let new_y = (transform.translation.y + delta_y).min(260.0);
 
transform.translation.y = new_y;

If the velocity is positive, the bird moves up, if the velocity is negative, the bird moves down.

Adding it to 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_plugins(MyPlugin)
        .run();
}

Let's run the game:

Flappy Bird Jump Flappy Bird Jump

As you can see, the bird rotates upwards when the player presses the space bar key.


We've successfully added the physics to the game, now the bird falls down and rotates based on the velocity and the bird can also jump.

In the next part, we'll add the movements to the pipes, we'll make them move towards the left side of the screen at a constant speed and reset their position once they move out of the screen, we'll also add collision detection between the bird and the pipes, and make the game stop when collision is detected.