Build a Flappy Bird Clone in Rust and Bevy 0.14Part 2 - Spawning game entities
In the previous part of the tutorial, we set up the project, installed Bevy and did the optimizations needed to run the game, we also downloaded the assets, initialized an empty Bevy App and created a Plugin to load the assets.
In this part, we will create the game entities and spawn them in the game, but before we start spawning the entities, let's first learn what are entities in Bevy.
What are entities?
In Bevy, an entity is a unique identifier that can be used to represent a game object. An entity can be anything in the game, in our case, it could be the bird, the pipes, or the ground. Entities are just unique identifiers and they don't hold any data, instead they group a set of components and the data is stored in the components.
Here's a simple visualization of how entities and components work in Bevy:
Entities are just unique identifiers that group a set of components.
Now suppose we want to implement the jump action of the bird, in order to do that we need to change the y
position of the bird on the screen, and for that we need something else called Systems.
Together, entities, components, and systems (ECS) are the building blocks of Bevy, and they work together to create a game.
Creating your first system
A system in Bevy is just a function that runs every frame and processes the entities and components. Systems are used to update the game state, handle input, and render the game.
The first system we need to create only needs to run once, only when the game starts, and it will spawn the game entities in the game.
Let's create a simple system that prints hello, world
at game start:
As we mentioned, systems are just a function:
Now let's add it to the Bevy App
:
Using the Startup
in the first argument tells bevy to only run the system once, at the start of the game.
Bevy Hello, World
Running the code will print hello, world
only once in the console.
Spawning entities
Now that we know what entities are and how to create a system, let's create the game entities and spawn them in the game.
In Bevy, we can create entities using the Commands
argument, which is a collection of commands that can be used to spawn entities, add components, and modify the game state.
Spawning a 2D Camera
Let's create a system called setup()
in this system, we'll spawn all of our game entities.
The first entity will be a 2D camera, without this camera, we won't be able to see anything in the game.
We can move the setup
function to a different file setup.rs
and import it in the main.rs
file.
Make sure to import the bevy prelude to setup.rs
:
Update main.rs
:
Let's run the code and see the camera in action:
Bevy Camera2dBundle
As you can see, the window is no longer black as the previous one, this is because we have added a 2D camera to the game.
The Background
Let's add a background to the game, if you have a look at the assets we downloaded, you'll find a background.png
image, we'll use this image as the background of the game.
Flappy Rust Background
The background will be a SpriteBundle
that contains a few components which we'll add in a moment, but we need a way to load the texture first.
Loading assets
Bevy gives us a way to load assets like textures and audio files using another argument which we can extract in the system function, this argument of type AssetServer
we can use it to load the textures, since AssetServer
is a resource, we need to wrap it in a Res struct, which will get us a shared borrow of the resource.
Let's get the AssetServer
from the argument:
Now that we have access to the assets, we can spawn the background and add a SpriteBundle
entity to it.
We are using the load method to load the texture from the path assets/texture/background.png
, notice that we don't need to type in the whole path, this is because Bevy expects the assets to be in the assets
folder by default.
The SpriteBundle
implements the Default trait which is in the standard library, this let's us use the ..default()
shorthand to set all the other fields to default except the texture
field.
Let's run the code and see the background in action:
Flappy Rust Background
Great! The background showed up in the window, but as you can tell, there's an issue: the width of the texture is only 288px
but the width of the whole screen is 800px
, so this results in the empty gaps on the sides of the window.
Background Scaling
Bevy let's us to provide more than one component to an entity, there's another component called ImageScaleMode which we can provide to the spawn
command to scale the image to fit the window.
In order to add multiple components in a single entity, we can instead of providing a component, provide multiple components as a tuple instead.
The ImageScaleMode has a variant called Tiled which repeats the texture to fill the whole screen.
We set tile_x
to true and tile_y
to false to only repeat on the x-axis, and we set the stretch_value
to 1.
to not stretch the texture.
Let's run the code:
Flappy Rust Background Tiled
What happened there? The image is exactly the same as before. The issue is that the size of the Sprite is set to the size of the texture, and since the texture is 288px
, the sprite will be 288px
as well.
Sprite custom size
Since we need to fill the whole screen (which is 800px
), we need to set the size of the sprite to the size of the window. We can do that by setting the custom_size
field of the sprite field on the SpriteBundle to the size of the window.
And since we have set WINDOW_WIDTH
as a constant in the previous part, we can just re-use that constant here.
custom_size
accepts an Option<Vec2>
type, therefore we also need to give the WINDOW_HEIGHT
as well in the second argument in the associated function.
We just updated the custom_size
field and used the ..default()
shorthand to set everything else to default, because Sprite
also implements Default
.
This change results in the background being repeated until it reaches the WINDOW_WIDTH
and fills the whole screen.
Flappy Rust Background Tiled
Great! Now the background shows up, but there's another simple step we need to do, which is adding a marker component to the background entity. This is important because we need to query the background later on in the other systems of the game.
Marker components are empty structs that are used to identify specific entities, they are useful when we need to query entities with specific components.
Background Component
In Bevy, we can add a marker component to an entity by adding a component with no fields, this is called a marker component, the struct itself does not hold any data, it is just a marker so that we can easily query it later on.
Don't worry if the term query does not make sense to you right now, it's just a way to get the entities in other parts (systems) of your code, we'll cover them later on, because we're gonna need them a lot.
Let's create a new file called components.rs
:
Update th main.rs
file:
Add a new struct called Background
to the components.rs
file:
We are using the Component derivable trait to derive the Component
trait for the Background
struct.
We then can import the Background
struct to the setup.rs
file and add it to the background entity as a new item in the tuple:
Now our Background entity has a marker component called Background
, we can use this marker component to query the background entity later on.
It looks complete now, but we might get back to it later on if we needed to do any changes.
The Ground
Spawning the ground is similar to spawning the background, the only difference is the texture and the position of the ground.
There's only one new thing we need to use, which is the Transform component, we can use the Transform::from_xyz()
method to set the position of the ground.
Try to do this yourself without using the Transform
and come back when you're done and check if you did it correctly.
From now on, we'll be creating the marker component first:
The spawn code in the setup()
function:
To make things easier, from now on, we'll import everything from the components.rs
file:
The code is pretty similar to the background, the only difference is the texture and the position of the ground.
We set the height to 112px
because the base.png
asset is 112px
high, and we don't want to repeat it on the y-axis, therefore we keep the original size on the y-axis but repeat on the x-axis
We used the Transform::from_xyz()
method to set the position of the ground, the x
position is 0
, meaning it's in the center on the x-axis, the y
position is -250.
, this moves the ground to the bottom, and the z
value is 1.
to make it appear on top of the background.
Let's run the code and see how the game looks like so far.
Flappy Rust Ground
Great! Now we have the base and the background set up for the game. Now, we're going to need to have some text on the screen, for example when the user opens up the game, we want to tell them what to press to start the game, so, let's implement that.
The texts
There are two texts we have in the assets, one of them is Game Over which will show up when the user loses the game, and the other one is Press Space Bar which will show up when the game starts and when the user loses the game.
Flappy Rust Game Over | Flappy Rust Press Space Bar |
---|
Game Over Text
Let's first spawn the Game Over text, we'll use the Visibility enum and set it to Hidden
Press Space Bar Text
Next, let's spawn the Press Space Bar text, it's going to be exactly like the Game Over text, except we move it down on the y-axis a bit.
We are setting the y
position to -50.0
to move it down by 50px
from the center.
Let's run the code and see how the game looks like so far.
Flappy Rust Texts
As you can see the Game Over text is hidden, this is because we set the visibility to Hidden
.
The Score
Spawning the score display is a little bit different, the texture we have is not a normal image like the other ones, but it's rather called a spritesheet or a texture atlas.
Flappy Rust Numbers Spritesheet
Texture Atlas
A texture atlas is an image that contains multiple smaller images that are packed together to reduce their overall size. In computer graphics, texture atlases are also called spritesheets or image sprites in 2D game development.
If there is a way for us to cut the spritesheet above into smaller individual numbers, we can then with the use of multiple sprites, represent the game score.
Thankfully, Bevy let's us use spritesheets in the game by providing the TextureAtlas struct, which we can use to load the spritesheet and get the individual sprites from it.
Just like every other component, let's first create the marker component for the score:
Let's create the sprite from the texture atlas, just like we did from the other textures:
We are transforming the numbers to the top left side of the screen, 350px
to left and 200px
to the top:
Let's run the code and see:
Flappy Rust Score
As you can see, all of the numbers are on screen, this is obviously not what we want, that's where the TextureAtlas comes in.
To make the sprite a texture atlas, we need to pass in a TextureAtlas
component to the sprite bundle, which takes in index
which is the index of the image section, and layout
which is a Handle<TextureAtlasLayout>
we need to create.
To add a texture atlas layout, we need to get the TextureAtlasLayout
from the function arguments, but we need to wrap it in the Assets
first and then wrap it in ResMut. ResMut
gives us a mutable reference to the resource.
We want the mutable reference because we're going to use a method called add()
which takes in &mut self
which uses a mutable reference, that's why we use ResMut
and not Res
We can then use the texture_atlas_layouts.add()
method to add the texture atlas layout to the assets, the add()
method takes in an argument of type TextureAtlasLayout
and returns an Handle<TextureAtlasLayout>
, so let's first create the TextureAtlasLayout
:
If you have the look at the numbers texture that we have, you can see that each number has a size, width of 24px
and height of 36px
, and there are 10
numbers in total.
We use the from_grid()
method to create the TextureAtlasLayout
and pass in a UVec2
which is a 2D vector of u32
, we also pass in the number of columns and rows respectively, the two None
values are padding
and offset
respectively that we don't need to pass in.
Since our texture has only 1 column and 10 rows, we pass in 1
and 10
respectively.
This creates the TextureAtlasLayout
we can then pass it to the add()
method and get the Handle<TextureAtlasLayout>
:
We then can pass the texture_atlas_layout
variable to the TextureAtlas
component.
We also set the index
to 0
to show the first number in the texture atlas at startup which is 0
.
Running the code:
Flappy Rust Score Atlas one digit
This is cool, but what if the number goes to double or triple digits? We have to have more than just one digit.
So, we need to spawn the same number multiple times, let's spawn it 3 times:
The starting point -350px
is the staring point of the first number, 24px
is the width of the number, and we add 2px
to have some space between the numbers.
Flappy Rust Score Atlas
Perfect! Now we have spawned the score display in the game, it's time to spawn the bird.
The Bird
Let's have a look at the bird asset.
Flappy Rust Bird
As you can see, this is another spritesheet so you know what to do, right?
The steps are easy:
- Create a marker component for the bird.
- Add a
SpriteBundle
to the entity. - Add a
TextureAtlas
to the entity. - Add the marker component to the entity.
Let's start by creating the marker component for the bird:
Flappy Rust Bird
At first it seemed daunting, but it's actually pretty simple.
The Pipes
If we have a look at the pipe asset, we can see that we only have one pipe, that's because we can use the same pipe for both the top and the bottom, we just need to rotate the pipe for the top one.
Let's create the components:
Spawning the pipes:
We are using the z
value of 0.5
to make it above the background and below the ground texture.
This will spawn the lower pipe:
Flappy Rust Lower Pipe
To spawn the upper pipe, we need to rotate the pipe by 180 degrees
and have a gap between itself and the lower pipe.
To change the rotation, we can use teh .rotate()
method, which mutates the Transform
object in place.
Therefore, we need to define a new variable transform
as mutable, and then rotate and use the mutated value as the transform
field of the SpriteBundle
.
Now, if we run it, we'll see the upper pipe too:
Flappy Rust Upper Pipe
This is great! But we also need more than just one pair of pipes, let's spawn 5.
This will spawn 5 pairs of pipes with a space of 200px
between each pair, we'll only be able to see the first pair of pipes because the other pairs are outside the window, so nothing will change in the window but having multiple pairs is important when the game starts and the pipes start moving.
Randomizing the Pipes
The code above only spawns pipes in the same position, we need to randomize the position to make the game more fun and challenging.
In order to do that, we'll use a crate called rand
which is a random number generator for Rust.
So, go ahead and add it to the Cargo.toml
file:
We want to generate the y
position of the pipes randomly, but it also has to be a range and not outside the window. We're going to write a helper function to generate the y
position of both the lower and upper pipes.
Let's create a utils.rs
file and add a function that generates a random number in that range:
Let's create a new function called random_pipe_position()
, with a little bit of trial and error, you can find the best position range for the lower pipe is from -70
to 280
, and the gap is 450px
, so for the upper pipe, we just add 450px
to the lower pipe's position.
Update main.rs
:
Great! Now we can use the random_pipe_position()
function to generate the random position of the pipes.
Let's run the code and see the pipes in action:
Flappy Rust Pipes
As you can see, from now on, the pipes will be generated in a random position while staying in the range of the window.
Conclusion
We have successfully spawned all of the game entities. While it may seem challenging at first, you'll soon realize that it's just a matter of assembling some code. Rust ensures you avoid errors, and Bevy efficiently organizes the game logic using its ECS architecture.
In the next part of the tutorial, we'll create the systems of the game, like when the user presses space and the bird jumps, or when the pipes move, and when the user loses the game. Systems make the game interactive and responsive to the user's input, you'll learn how to create systems, query the entities we made, and update the game state.