Build a Flappy Bird Clone in Rust and Bevy 0.14Part 6 - Pipe movements
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
asGameOver
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.
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
-
Query Conversion:
upper_pipe_query.iter()
converts the query results into an iterator, enabling the use of iterator methods likemax_by
. -
Finding the Maximum:
.max_by(|(_, a), (_, b)| a.translation.x.partial_cmp(&b.translation.x).unwrap())
iterates through the pipes, comparing theirx
positions to find the pipe with the highestx
value. -
Unwrapping the Result:
.unwrap()
ensures that we safely extract the result of themax_by
method. This is safe because we assume there is at least one pipe in the query. -
Accessing the Transform:
.1
accesses the second element of the tuple, which is the transform component containing the position data. -
Getting the
x
Position:.translation.x
retrieves thex
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
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!