Build a Flappy Bird Clone in Rust and Bevy 0.14Part 3 - Adding systems
Bird Animation
In the previous part of the tutorial, we learned how to spawn the game entities, transform their positions, give them marker components, and render them on the screen.
However, the objects on the screen don't move. They are static and can't be interacted with. There needs to be some kind of interaction between the player and the game entities. This is where systems come into play.
In this part, we will learn how to add systems to our game.
If you remember when we added the setup
system in the previous part to the Bevy App
in the main.rs
file, we used the method add_systems
with the first argument being the schedule which was Startup
that makes the function only run at startup.
This time however, we need to run the function at every frame. To do this, instead of Startup
, we use Update
as the first argument.
Let's start by adding a simple system that makes the Press Space Bar text blink, instead of being static.
Let's create a new file called systems.rs
in the src
directory, feel free to have your own directory structure if you want:
Update main.rs
:
Text Blinking System
In order to create the blinking animation, we need a way to calculate the time, to know how many seconds or milliseconds have passed, without knowing the time, we wouldn't be able to know when to hide the text and when to show it.
We need a mechanism to calculate the time every-time the system runs on Update
(which is in very small fractions of a second), and only to show/hide the text when a certain amount of time has passed.
To calculate the time, we can use the Time struct which is available in the prelude, so we don't need to import it.
Since it's a resource, we need to wrap it in a Res struct, and since we do not need to mutate it, we don't use ResMut
.
With the Time struct, we can get the amount of time that has passed since the last frame using the delta()
method.
Let's log the delta to the console using the dbg!
macro:
And, don't forget to add it to the entry point of the game in main.rs
and set the schedule
to Update
:
Running the code:
Time Delta
The code runs roughly every 16 milliseconds, which is the time it takes to render a frame at 60 frames per second.
It's great that we can track the time, but there should also be a timer and only if that timer was finished, we should change the state of the text, otherwise we shouldn't do anything.
Bevy provides us with the Timer struct, with the conjunction the Time
, we can find out when the timer has finished.
But, first we need to add the timer to the Entity
we created in the previous part, we need to go back there and add the timer to the entity, so that we can use it here in the system.
In your components.rs
file, update the PressSpaceBarText
struct to include the Timer
:
In the setup.rs
file, add the timer to the PressSpaceBarText
entity:
The timer is set to 0.5
seconds, and the mode is set to Repeating, which means that the timer will repeat every 0.5
seconds.
Now, in the system, we need a mechanism to get mutable access to both PressSpaceBarText
and Visibility
components, so we can change the state of the timer and show/hide the text.
In order to do that, we need to first understand how to use Query in Bevy.
Understanding Query in Bevy
In Bevy, the Query
is a powerful tool to get the components of the entities that match the query. It's a way to get the components of the entities that we are interested in.
The Query
is a generic that takes in two parameters, the first one is the components you want to get, and the second one is an optional filter that you can use to filter the entities.
Each parameter can be a tuple or it can just be a single component.
To get each component, you either have to get immutable or mutable access to the component, specified by &
or &mut
respectively.
The second parameter is used to filter the entities, you will not be able to have access to the components you specify there, if you want to have access, you need to pass them in the first parameter.
In our case, we want to get mutable access to the PressSpaceBarText
and Visibility
components, so we pass them in the first parameter.
We don't need to use the filter parameter, because we only have one entity with the PressSpaceBarText
component and we can guarantee that there is only one entity with the PressSpaceBarText
component.
Great! Now we have mutable access to both visibility and timer states. We can tick the timer on every Update
and change the visibility of the text based on the timer.
Since, we only have one entity with the PressSpaceBarText
component, we can use the single_mut
method on the query to get the mutable reference to the entity.
The single_mut()
method gives us a mutable reference to the components, but it panics if there are more than one entity coming back from the query, so make sure you only use it when you can guarantee that there is only one entity matching the query.
Tick the timer by time.delta()
:
This will add the delta time which is the time that has passed since the last run of the system, and updates the timer.
Checking if the Timer is Finished
Now, we need to check if the timer is finished, if it is, we need to change the visibility of the text.
The finished()
method returns true
if the timer has finished with the summation of all the tick()
calls, reached the duration that we first defined in the setup.rs
file, which was 0.5
seconds.
If the timer is finished, we need to change the visibility of the text, if it's visible, we need to hide it, and if it's hidden, we need to show it.
Use the dereference operator *
to modify the visibility of the text.
Perfect! If you run the code, you'll see that the text is blinking every 0.5 seconds.
Moving the background
It's important to give the player a sense of movement in the game, to do that, we need to move the background to the left when the game starts.
Let's query the background's transform component and move it to the left by a certain amount every frame.
In the systems.rs
file, add a new system called move_background
:
Since we don't need to access any data on the Background
struct (it's just a marker component), we only use it in the query filter to get the Transform
component of the background.
Update main.rs
file:
Running the code, you'll see that the background is moving to the left.
Moving Background
As you can see, the background is moving to the left and it's moving outside the screen.
To fix that, there's a few things we need to do:
- Make the background width larger so that we don't see the gap.
- Reset the background position to the initial position when it moves outside the screen by background texture width * 2.
The texture width is 288px
, so when the background moves by 288px
, we need to reset the position of the background to the normal position.
The reason we need to multiply the texture width by 2 is because when we increase the width of the background, we increase 288px / 2
on the left and 288px / 2
on the right, that means there will still be a gap of 288px / 2
on the right side, to fix that, we need to multiply the texture width by 2.
Resetting the position of the background:
If we run the code this time, we'll see that the background is moving to the left and when it moves outside the screen, it resets to the initial position. This gives us the feeling that the background is moving infinitely, but in actuality, it's just resetting the position.
Moving the ground
Let's add some movement to the base as well but with a different speed from the background. Since the ground is closer to the camera perspective, it should move faster than the background.
The code will be quite similar to the background movement, and it has the same width of 288px
, so we can use the same logic to reset the position.
Add a new system called move_ground
in the systems.rs
file:
Update the sprite width:
Add it to main.rs
:
Running the code, you'll see that the ground is moving to the left a little bit faster than the background.
Moving Ground
Animating the Bird
If you remember in the previous part, we talked about Spritesheets which is a collection of images in a single image file. The spritesheet for the bird contained three textures, one for each frame of the bird's animation.
Now, we need to use these textures to animate the bird, we change it to a different texture at a certain interval, which means we're going to use a Timer.
We worked with timers before, so this is going to be quite easy.
Let's update the bird component and add a Timer
to it:
The reason we're using named fields is because we will add more fields to the Bird
component in the next steps, so we use named fields instead of tuple fields to make it easier to read and understand.
Updating the spawn code in the setup.rs
file:
We added the timer with a duration of 0.2
seconds.
Create the system to animate the bird:
What we need to query is the TextureAtlas
because we want to change the index of the texture atlas when the timer finishes, we also need to query the bird so that we can tick the timer and check if the timer has finished.
We also used the Time to get the delta time and tick the timer.
In this system we have used iter_mut()
instead of single_mut()
, if you want to use single_mut()
, you can go ahead and do it, it will be exactly the same as this, the only difference is that if there are more than one entity returned in the query, single_mut()
is going to panic.
Run the code and you'll see a cool animation of the bird flapping its wings.
Bird Animation
That's it for this part of the tutorial, we learned how to add systems to our game, how to use timers, query components, and adding animations to the game.
In the next part of the tutorial, we're going to learn about Resources and how to use them. After we add resources, we'll add more systems to make the game a little bit more interactive.