Spawning game entities

Build a Flappy Bird Clone in Rust and Bevy 0.14Part 2 - Spawning game entities

Game
Bevy 0.14
Flappy Bird
Last update:

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:

Bird Entity
  - Transform Component (Position, Rotation, Scale)
  - Sprite Component (Texture, Material)
  - Velocity Component (Speed, Direction)
 
Pipe Entity
  - Transform Component (Position, Rotation, Scale)
  - Sprite Component (Texture, Material)
  - Velocity Component (Speed, Direction)

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.

Learn Rust by Practice

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

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:

fn say_hello_world() {
    println!("hello, world");
}

Now let's add it to the Bevy App:

fn main() {
    App::new()
        .add_systems(Startup, say_hello_world)
        .add_plugins(MyPlugin)
        .run();
}

Using the Startup in the first argument tells bevy to only run the system once, at the start of the game.

Bevy Hello, World 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.

fn setup(mut commands: Commands) {
    // Spawn a 2D camera
    commands.spawn(Camera2dBundle::default());
}
fn main() {
    App::new()
        .add_systems(Startup, setup)
        .add_plugins(MyPlugin)
        .run();
}

We can move the setup function to a different file setup.rs and import it in the main.rs file.

src
├── main.rs
├── setup.rs
├── plugin.rs
└── constants.rs

Make sure to import the bevy prelude to setup.rs:

// setup.rs
use bevy::prelude::*;
 
pub fn setup(mut commands: Commands) {
    // Spawn a 2D camera
    commands.spawn(Camera2dBundle::default());
}

Update main.rs:

// main.rs
use bevy::prelude::*;
use plugin::MyPlugin;
use setup::setup;
 
mod constants;
mod plugin;
mod setup;
 
fn main() {
  ...
}

Let's run the code and see the camera in action:

cargo run

Bevy Camera2dBundle 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 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:

use bevy::prelude::*;
 
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // Spawn a 2D camera
    commands.spawn(Camera2dBundle::default());
}

Now that we have access to the assets, we can spawn the background and add a SpriteBundle entity to it.

pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // Spawn a 2D camera
    commands.spawn(Camera2dBundle::default());
 
    // Spawn the background
    commands.spawn(SpriteBundle {
        texture: asset_server.load("texture/background.png"),
        ..default()
    });
}

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:

cargo run

Flappy Rust Background 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.

commands.spawn((
    SpriteBundle {
        texture: asset_server.load("texture/background.png"),
        transform: Transform::from_xyz(0., 0., 0.),
        ..default()
    },
    ImageScaleMode::Tiled {
        tile_x: true, // Only repeat on the x-axis
        tile_y: false, // no repeat on the y-axis
        stretch_value: 1., // no stretching
    },
));

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 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.

use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
 
commands.spawn((
    SpriteBundle {
        texture: asset_server.load("texture/background.png"),
        sprite: Sprite {
            custom_size: Some(Vec2::new(WINDOW_WIDTH, WINDOW_HEIGHT)), // Adding a custom size
            ..default() // Everything else is set to default
        },
        ..default()
    },
    ImageScaleMode::Tiled {
        tile_x: true,
        tile_y: false,
        stretch_value: 1.,
    },
));

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 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:

src
├── components.rs
├── main.rs
├── setup.rs
├── plugin.rs
└── constants.rs

Update th main.rs file:

use bevy::prelude::*;
use plugin::MyPlugin;
use setup::setup;
 
mod components;
mod constants;
mod plugin;
mod setup;

Add a new struct called Background to the components.rs file:

use bevy::prelude::*;
 
#[derive(Component)]
pub struct Background;

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:

use bevy::prelude::*;
 
use crate::{
    components::Background,
    constants::{WINDOW_HEIGHT, WINDOW_WIDTH},
};
 
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    ...
    // Spawn the background
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/background.png"),
            sprite: Sprite {
                custom_size: Some(Vec2::new(WINDOW_WIDTH, WINDOW_HEIGHT)), // Adding a custom size
                ..default() // Everything else is set to default
            },
            ..default()
        },
        ImageScaleMode::Tiled {
            tile_x: true,
            tile_y: false,
            stretch_value: 1.,
        },
        Background,
    ));
}

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:

// components.rs
#[derive(Component)]
pub struct Ground;

The spawn code in the setup() function:

use crate::{
    components::{Background, Ground},
    constants::{WINDOW_HEIGHT, WINDOW_WIDTH},
};
 
 
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    ...
    // Spawn the Ground
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/base.png"),
            sprite: Sprite {
                custom_size: Some(Vec2::new(WINDOW_WIDTH, 112.)),
                ..default()
            },
            transform: Transform::from_xyz(0., -250., 1.),
            ..default()
        },
        ImageScaleMode::Tiled {
            tile_x: true,
            tile_y: false,
            stretch_value: 1.,
        },
        Ground,
    ));
}

To make things easier, from now on, we'll import everything from the components.rs file:

// setup.rs
use crate::{
    components::*,
    constants::{WINDOW_HEIGHT, WINDOW_WIDTH},
};

The code is pretty similar to the background, the only difference is the texture and the position of the ground.

custom_size: Some(Vec2::new(WINDOW_WIDTH, 112.)),

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

transform: Transform::from_xyz(0., -250., 1.),

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 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 Game OverFlappy Rust Press Space Bar 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

// components.rs
#[derive(Component)]
pub struct GameOverText;
// setup.rs
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    ...
    // Game Over Text
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/game-over.png"),
            transform: Transform::from_xyz(0., 0., 1.),
            visibility: Visibility::Hidden,
            ..default()
        },
        GameOverText,
    ));
}

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.

// components.rs
#[derive(Component)]
pub struct PressSpaceBarText;
// setup.rs
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    ...
    // Space Bar Text
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/space.png"),
            transform: Transform::from_xyz(0.0, -50.0, 1.0),
            ..default()
        },
        PressSpaceBarText,
    ));
}

We are setting the y position to -50.0 to move it down by 50px from the center.

transform: Transform::from_xyz(0.0, -50.0, 1.0),

Let's run the code and see how the game looks like so far.

Flappy Rust Texts 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 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:

// components.rs
#[derive(Component)]
pub struct ScoreText;

Let's create the sprite from the texture atlas, just like we did from the other textures:

// setup.rs
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/numbers.png"),
            transform: Transform::from_xyz(-350., 200., 1.),
            ..default()
        },
        ScoreText,
    ));
}

We are transforming the numbers to the top left side of the screen, 350px to left and 200px to the top:

transform: Transform::from_xyz(-350., 200., 1.),

Let's run the code and see:

Flappy Rust Score 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.

commands.spawn((
    SpriteBundle {
        texture: asset_server.load("texture/numbers.png"),
        transform: Transform::from_xyz(-350., 200., 1.),
        ..default()
    },
    TextureAtlas {
        index: 0,
        layout: // We need to create the handle
    },
    ScoreText,
));

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

// setup.rs
pub fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
    ...
}

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:

let number_layout = TextureAtlasLayout::from_grid(UVec2::new(24, 36), 1, 10, None, None);

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>:

let number_layout: TextureAtlasLayout =
    TextureAtlasLayout::from_grid(UVec2::new(24, 36), 1, 10, None, None);
let number_texture_atlas_layout: Handle<TextureAtlasLayout> =
    texture_atlas_layouts.add(number_layout);

We then can pass the texture_atlas_layout variable to the TextureAtlas component.

    let number_layout: TextureAtlasLayout =
        TextureAtlasLayout::from_grid(UVec2::new(24, 36), 1, 10, None, None);
    let number_texture_atlas_layout: Handle<TextureAtlasLayout> = texture_atlas_layouts.add(number_layout);
 
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/numbers.png"),
            transform: Transform::from_xyz(-350., 200., 1.),
            ..default()
        },
        TextureAtlas {
            index: 0,
            layout: number_texture_atlas_layout,
        },
        ScoreText,
    ));

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 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:

    for i in 0..3 {
        let starting_point = -350. + (i as f32 * (24. + 2.)); // 24 is the width + 0.2 is the space between the numbers
 
        commands.spawn((
            SpriteBundle {
                texture: asset_server.load("texture/numbers.png"),
                transform: Transform::from_xyz(starting_point, 200., 1.),
                ..default()
            },
            TextureAtlas {
                index: 0,
                layout: number_texture_atlas_layout.clone(),
            },
            ScoreText,
        ));
    }

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 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 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:

// components.rs
#[derive(Component)]
pub struct Bird;
 
// setup.rs
pub fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
    ...
    // Spawn the bird
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/bird.png"),
            transform: Transform::from_xyz(0., 0., 2.),
            ..default()
        },
        TextureAtlas {
            index: 1,
            layout: texture_atlas_layouts.add(TextureAtlasLayout::from_grid(
                UVec2::new(34, 24),
                3,
                1,
                None,
                None,
            )),
        },
        Bird,
    ));
}

Flappy Rust 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:

// components.rs
#[derive(Component)]
pub struct UpperPipe;
 
#[derive(Component)]
pub struct LowerPipe;

Spawning the pipes:

// setup.rs
// Spawn Lower Pipe
commands.spawn((
    SpriteBundle {
        texture: asset_server.load("texture/pipe.png"),
        transform: Transform::from_xyz(350., -200., 0.5),
        ..default()
    },
    LowerPipe,
));

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 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.

let mut transform = Transform::from_xyz(350., 250., 0.5);
transform.rotate(Quat::from_rotation_z(std::f32::consts::PI));
 
// // Spawn Lower Pipe
commands.spawn((
    SpriteBundle {
        texture: asset_server.load("texture/pipe.png"),
        transform,
        ..default()
    },
    LowerPipe,
));

Now, if we run it, we'll see the upper pipe too:

Flappy Rust Upper Pipe Flappy Rust Upper Pipe

This is great! But we also need more than just one pair of pipes, let's spawn 5.

// setup.rs
for i in 0..5 {
    let delta_x = i as f32 * 200.; // Space between pairs of pipes
    let mut transform = Transform::from_xyz(350. + delta_x, -250., 0.5);
 
    // Spawn Lower Pipe
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/pipe.png"),
            transform,
            ..default()
        },
        LowerPipe,
    ));
 
    // Rotating the upper pipe
    transform.rotate(Quat::from_rotation_z(std::f32::consts::PI));
    // Changing the y position of the upper pipe
    transform.translation.y += 450.;
 
    // Spawn Upper Pipe
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/pipe.png"),
            transform,
            ..default()
        },
        UpperPipe,
    ));
}

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:

[dependencies]
bevy = { version = "0.14", features = ["dynamic_linking"] }
rand = "0.8"

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:

src
├── components.rs
├── main.rs
├── setup.rs
├── plugin.rs
├── constants.rs
└── utils.rs

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.

// utils.rs
use rand::Rng;
 
pub fn random_pipe_position() -> (f32, f32) {
    let mut rng = rand::thread_rng();
    let lower = -rng.gen_range(70.0..280.0); // Lower pipe position (negative)
 
    (lower, lower + 450.0)
}

Update main.rs:

use bevy::prelude::*;
use plugin::MyPlugin;
use setup::setup;
 
mod components;
mod constants;
mod plugin;
mod setup;
mod utils;
 
fn main() {
    ...
}

Great! Now we can use the random_pipe_position() function to generate the random position of the pipes.

for i in 0..5 {
    let delta_x = i as f32 * 200.;
    let (lower_y, upper_y) = random_pipe_position();
    let mut transform = Transform::from_xyz(350. + delta_x, lower_y, 0.5);
 
    // Spawn Lower Pipe
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/pipe.png"),
            transform,
            ..default()
        },
        LowerPipe,
    ));
 
    transform.rotate(Quat::from_rotation_z(std::f32::consts::PI));
    transform.translation.y = upper_y;
 
    // Spawn Upper Pipe
    commands.spawn((
        SpriteBundle {
            texture: asset_server.load("texture/pipe.png"),
            transform,
            ..default()
        },
        UpperPipe,
    ));
}

Let's run the code and see the pipes in action:

Flappy Rust Pipes 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.