Want more Rust and Bevy content? Explore the Rust Programming Academy where you can find a comprehensive, project-based learning pathway on it!
Unlock the full potential of the Bevy engine with our in-depth Bevy tutorial – and start building blazing-fast, visually stunning games in no time!
In this guide, we will walk through building a game project using Bevy, a modern game engine designed around the Entity Component System (ECS) architecture and written in Rust. Along the way, you’ll learn the fundamental concepts of Rust and how Bevy leverages them to create efficient, scalable, and high-performing games.
Throughout this tutorial, we will cover the following:
- Setting up your Rust and Bevy environment
- Understanding ECS architecture and its advantages
- Building a modular project structure
- Defining components, constants, and bundles
- Implementing a basic game window and camera functionality
Additionally, you can review our full course, Build Your First Game with Rust and Bevy which covers these concepts in-depth.
Let’s get started with this Bevy tutorial.
Project Files
To make your learning experience easier, we’ve included the project files used in this Bevy tutorial. While you’ll still need to follow the steps to set up your own project, these resources will help guide you through the process and provide a reference. Download project files here: Bevy Tutorial Project Files
Rust Basics
Obviously in our Bevy tutorial, we’re going to explore Bevy – a simple data-driven game engine built in Rust. First, though, let’s dive into some core concepts that make Rust and Bevy powerful tools for game development.
Why Rust for Game Development?
- Rust is designed for system-level programming, offering low-level control without sacrificing high-level convenience.
- Rust’s ownership model guarantees thread safety and enables fearless concurrency, allowing you to write concurrent code more easily.
- Rust ensures memory safety and eliminates data races while maintaining performance close to that of C.
Why Choose Bevy?
- Bevy uses ECS (Entity Component System) architecture, which decouples entities, components, and systems, making it easy to manage game state logic.
- Cross-platform deployment: Write your game once and deploy it across multiple platforms, including Windows, Mac, Linux, and web.
- Bevy provides powerful tools for real-time rendering, making it suitable for graphic-intensive applications.
Key Concepts in Rust
- Rust ensures memory safety without a garbage collector. Each value has a single owner, and when the owner goes out of scope, the value is dropped.
- Rust allows references to data without taking ownership.
- Lifetimes prevent dangling references, ensuring data is safely accessed.
- Rust includes a powerful feature with match statements for handling different cases, making the code more expressive and robust.
Entity Component System (ECS) in Bevy
- Entity: A unique identifier for an object in the game. Think of them as game objects.
- Components: Data containers for specific attributes of entities, such as position, velocity, or health.
- Systems: Logic processes that operate on entities with specific components, driving the behavior and interaction in your game.
This separation of data and behavior allows for a flexible and efficient game design.
Basic Syntax in Rust for Bevy Game Development
mut
: Makes a variable mutable. By default, variables are immutable in Rust.fn
: Defines functions. Functions can have parameters and return values.struct
: Creates custom data types. Structs are used to bundle related data together.enum
: Defines a type by enumerating its possible values. Enums are used for types that can be one of several variants.
Primary Scalar Types in Rust
- Integers
- Floating point numbers
- Booleans
- Characters
Understanding these basic data types is crucial for managing and manipulating data effectively in your Rust programs.
By mastering these concepts and syntax, you’ll be well on your way to creating powerful and efficient games using Bevy and Rust.
Set Up
In this first part of our Bevy tutorial proper, let’s go ahead and set up our Bevy project. Before we start, you must ensure that you have Rust installed. If not, you can always get it from the official web page.
Setting Up the Bevy Project
To begin, open the terminal in the directory where you want to create your game. Utilize the command cargo new my_bevy_game
and navigate inside this folder.
cargo new my_bevy_game
Let’s go ahead and open this in our text editor, which is Visual Studio Code in my case. Here we can see that our project has been created with a source directory containing a main.rs
file. And in here, there’s only one function, which is to print the line “hello world.
fn main() { println!("hello world"); }
In the Cargo.toml
file, it contains information about the nested packages and dependencies we need for this game. Under the dependencies, we want to add bevy = "0.14"
, which is the version we’ll be utilizing.
[dependencies] bevy = "0.14"
Open the terminal once again, we utilize cargo run
. This is a command provided by Cargo, the Rust package management build system. This will compile our Rust project, and through all the source files are up to date. It compiles both the project code and the dependencies listed in our Cargo.toml
file. As this is the first time running, it will need to fetch all the dependencies, so this might take a while. But once it has finished, we can see that it has printed the line “hello world”.
cargo run
This completes the setup of our Bevy project.
Want to unlock the power of Rust for game development with Bevy? Our Rust Programming Academy offers hands-on projects to help you build a portfolio of Rust-powered projects, in-depth video tutorials to guide you through the Rust ecosystem, and more.
Project Structure
Next in our Bevy tutorial, we’ll be focusing on the project structure, which is designed to modularize the code for better organization, maintainability, and scalability. We’ll be organizing the project in a way that aligns with the Entity-Component-System (ECS) architecture we previously mentioned. This structure will help ensure that the code is easy to follow and manage as the game grows in complexity.
Project Structure Overview
The project structure is designed to separate concerns and centralize related functionalities. Here’s an overview of the key files and directories we’ll be creating:
Components
The first file we want to create is components.rs
. This file will define all the ECS components used in the game, such as player, ball, and paddle. Centralizing these components ensures a single place to manage all the data types attached to entities, making it easier to track and modify.
Systems Directory
Next, we want to create a systems
directory. This directory will contain all the individual modules for different systems in the game. Each module handles a specific aspect of the game logic, ensuring separation of concerns.
mod.rs
: The module file that re-exports all the systems. This allows easy import of all systems inmain.rs
just by referencing the system module.ball.rs
: Contains systems related to the ball’s behavior, such as spawning, movement, and scoring detection.paddle.rs
: Manages the system for the paddle behavior, including input from player one and player two.scoreboard.rs
: Handles systems related to the scoreboard, such as updating and displaying scores.collision.rs
: Contains the collision system and logic to handle interactions between the ball and paddle.
Bundles
Within the source directory, we create a bundles.rs
file. This file defines the entity bundles, which are collections of components that are frequently used together. For example, a ball bundle might include ball and position components. Using these bundles helps streamline entity creation, ensuring that all the required components are included.
Constants
Lastly, we have the constants.rs
file, which contains the constant values used throughout the game, such as speed, dimensions, and other configurations. Centralizing these values ensures that changes to these values are easy to manage and propagate throughout the codebase.
Benefits of This Project Structure
This overall project structure allows us to break down the code into modules based on functionality. By separating components, systems, and bundles, each file has a clear responsibility. This helps manage complexity and reduces the likelihood of errors. As the game grows in complexity, new features and systems can be added without disrupting the existing code significantly.
Summary
In summary, organizing your project structure in this modular way aligns with the ECS architecture and promotes better organization, maintainability, and scalability. By following this structure, you’ll find it easier to manage and extend your game as it grows.
Components
Now that we’ve created our project structure and laid the foundation for our Bevy tutorial, let’s start coding by defining our components.
Imports
We begin by making the necessary imports with `use bevy::prelude::*;` and `use bevy::math::Vec2;`. These imports bring in all the common Bevy types and functions, as well as the `Vec2` type from Bevy’s math module. `Vec2` is a vector type with two components, x and y, which is commonly used for 2D coordinates.
use bevy::prelude::*; use bevy::math::Vec2;
Defining Components
Next, we want to utilize the `#[derive(Component)]` macro. This macro automatically implements the necessary traits for the structure to be used as a component in Bevy’s Entity Component System (ECS).
Player Components
The first component we define is `Player1`, which will serve as a tag or marker component to identify entities representing player 1. Similarly, we define `Player2` for player 2.
#[derive(Component)] pub struct Player1; #[derive(Component)] pub struct Player2;
Score Components
We also define components to keep track of the scores for each player:
#[derive(Component)] pub struct Player1Score; #[derive(Component)] pub struct Player2Score;
Scorer Enum
Next, we define a public enum `Scorer` with two variants, `Player1` and `Player2`. This will be used to indicate which player scored.
pub enum Scorer { Player1, Player2, }
Game Object Components
We then define components for the game objects, which will be a ball, a paddle, and the boundary that contains everything:
#[derive(Component)] pub struct Ball; #[derive(Component)] pub struct Paddle; #[derive(Component)] pub struct Boundary;
Position and Velocity Components
We also define components to store the position and velocity of entities. This is where our Vec2 import will come into play:
#[derive(Component)] pub struct Position(pub Vec2); #[derive(Component)] pub struct Velocity(pub Vec2);
Shape Component
The `Shape` component is used to store the size or dimensions of an entity in a 2D space:
#[derive(Component)] pub struct Shape(pub Vec2);
Scored Event
We define an event `Scored` that takes in the `Scorer` enum. This event is used to create events indicating that a player has scored:
#[derive(Event)] pub struct Scored(pub Scorer);
Score Resource
Finally, we define a `Score` resource that will keep track of the scores for both players. This resource is derived from `Resource` and `Default` traits:
#[derive(Resource, Default)] pub struct Score { pub player1: u32, pub player2: u32, }
By defining these components and resources, we have laid the groundwork for our game’s entities and their properties.
Constants
Now that we’ve created the different components needed for our Bevy tutorial game, let’s define all the constants, which will be utilized throughout our development. Constants are essential as they allow us to easily manage and update values that are used frequently in our code.
Defining Constants
In Rust, we use the pub
keyword to make an item public, meaning it’s accessible by other modules. Let’s start by defining the size of the popcorns (balls) and the paddle dimensions.
Ball Size
We’ll define the size of the ball as a floating-point number (f32
).
pub const BALL_SIZE: f32 = 5.0;
Paddle Dimensions
Next, we’ll define the width and height of the paddles. Initially, we’ll set both to 5, but we’ll adjust the paddle width to 10 and the height to 50 to make it a rectangle shape.
pub const PADDLE_WIDTH: f32 = 10.0; pub const PADDLE_HEIGHT: f32 = 50.0;
Speeds
We also need to define the speeds for the ball and the paddles. These speeds will determine how fast the ball and paddles move in the game.
pub const BALL_SPEED: f32 = 5.0; pub const PADDLE_SPEED: f32 = 5.0;
Boundary Height
Lastly, we’ll define the height of the boundaries at the top and bottom of the game window. This will act as a border for our game.
pub const BOUNDARY_HEIGHT: f32 = 20.0;
Complete Constants File
Here is the complete constants.rs
file with all the defined constants:
pub const BALL_SIZE: f32 = 5.0; pub const PADDLE_WIDTH: f32 = 10.0; pub const PADDLE_HEIGHT: f32 = 50.0; pub const BALL_SPEED: f32 = 5.0; pub const PADDLE_SPEED: f32 = 5.0; pub const BOUNDARY_HEIGHT: f32 = 20.0;
By defining these constants, we make our code more maintainable and easier to update. If we need to change the size of the ball or the speed of the paddles, we only need to update the values in this file, and the changes will be reflected throughout the entire game.
Ready to take your Rust skills to the next level with Bevy? Our Rust Programming Academy offers in-depth video tutorials on building high-performance projects with Rust and expert guidance on building games, web apps, and more.
Bundles
In this next part of our Bevy tutorial, we will create bundles in Bevy, which are collections of components that are often used together. Bundles help in reducing boilerplate code when spawning different entities. We will define and implement bundles for the ball, paddle, and boundary entities in our game.
Importing Necessary Modules
First, let’s begin by making the necessary imports. We need to import Bevy’s prelude, math module for vector operations, and our custom components and constants modules.
use bevy::prelude::*;
use bevy::math::Vec2;
use crate::components::*;
use crate::constants::*;
Defining the Ball Bundle
Bundles in Bevy are defined using the #[derive(Bundle)]
attribute. This attribute provides the necessary traits for the struct to be used as a bundle in Bevy’s ECS (Entity Component System). We’ll start by making our ball bundle, for which we’ll need to know it’s shape, velocity, and position. As such, our bundle will attach those components to our ball.
#[derive(Bundle)] pub struct BallBundle { pub ball: Ball, pub shape: Shape, pub velocity: Velocity, pub position: Position, }
Next, we implement the BallBundle
with a constructor function to create a new instance of the bundle. The shape will use our constants variables in this case since we already defined the ball size.
impl BallBundle { pub fn new(x: f32, y: f32) -> Self { Self { ball: Ball, shape: Shape(Vec2::new(BALL_SIZE, BALL_SIZE)), velocity: Velocity(Vec2::new(x, y)), position: Position(Vec2::new(0.0, 0.0)), } } }
Defining the Paddle Bundle
Similarly, we define the PaddleBundle
for the paddle entity, which also needs a shape, position, and velocity component.
#[derive(Bundle)] pub struct PaddleBundle { pub paddle: Paddle, pub shape: Shape, pub position: Position, pub velocity: Velocity, }
We also implement the PaddleBundle
with a constructor function to create an instance. Likewise to the ball bundle, we can use our constants variables to set the shape of the bundle.
impl PaddleBundle { pub fn new(x: f32, y: f32) -> Self { Self { paddle: Paddle, shape: Shape(Vec2::new(PADDLE_WIDTH, PADDLE_HEIGHT)), position: Position(Vec2::new(x, y)), velocity: Velocity(Vec2::new(0.0, 0.0)), } } }
Defining the Boundary Bundle
Finally, we define the BoundaryBundle
for the boundary entity. In this case, as our boundary won’t move, it doesn’t need a velocity component.
#[derive(Bundle)] pub struct BoundaryBundle { pub boundary: Boundary, pub shape: Shape, pub position: Position, }
We implement the BoundaryBundle
with a constructor function that takes the width as a parameter, allowing for dynamic window sizes.
impl BoundaryBundle { pub fn new(x: f32, y: f32, width: f32) -> Self { Self { boundary: Boundary, shape: Shape(Vec2::new(width, BOUNDARY_HEIGHT)), position: Position(Vec2::new(x, y)), } } }
Summary
In this part of our Bevy tutorial, we created bundles for the ball, paddle, and boundary entities in our game. These bundles help in reducing boilerplate code when spawning entities by grouping related components together.
Camera
Continuing with our Bevy tutorial, we will focus on setting up the camera in our Bevy game. Let’s get started by opening the camera.rs
file.
Importing Bevy’s Preload
First, we begin by importing Bevy’s preload once again. This is essential for loading resources in our game.
use bevy::prelude::*;
Creating the Spawn Camera Function
Next, we want to create a public function called spawn_camera
, which takes in a mutable commands
parameter. The commands
parameter is a Bevy resource that allows you to create, modify, or delete entities and their components.
pub fn spawn_camera(mut commands: Commands) { commands.spawn(Camera2dBundle::default()); }
In this function, we use commands.spawn
to create a new entity of the default 2D camera. This will include the camera component and a transform component that positions the camera.
Understanding Cameras in Bevy
In Bevy, cameras are entities that have one or more components defining their properties. They allow us to render the view, meaning the camera determines what part of the game world is visible on the screen. The transform component of the camera defines its position and orientation in the game world. Bevy supports multiple cameras, each potentially rendering different parts of the game world or used for different purposes, such as split-screen or multiplayer.
Updating the Main Function
Now, let’s move to the main.rs
file to make some necessary updates to initialize our app. We begin by making the necessary imports:
use bevy::prelude::*; mod systems; use systems::*;
Initializing the App
We remove the print line statement and modify the code to initialize our app:
fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup) .run(); }
Here, we create a new instance of the app using App::new()
. We then add the default plugins, which provide essential functionalities required for a typical game, including window creation, event handling, input management, and rendering.
Adding Systems
Next, we add systems and specify that the spawn_camera
system will be modified at the start of the game:
.add_systems(Startup, spawn_camera)
Modifying the Modules
We also need to modify the mods.rs
file to define and re-export the necessary modules:
pub mod ball; pub mod paddle; pub mod camera; pub use ball::*; pub use paddle::*; pub use camera::*;
This makes the components, bundles, and systems accessible from outside the current module.
Finalizing the Main Function
Heading back to the main.rs
file, we use .run()
to start running the app. Our final function will look something like this:
fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, spawn_camera) .run(); }
Running the App
Once we run the app using cargo run
, we can see that a new window has loaded, displaying our game with the camera set up.
Want to create stunning games with Bevy and Rust? Our Rust Programming Academy provides hands-on projects to help you build a portfolio of visually stunning Rust projects to solidify your understanding of Rust, Bevy, and a variety of other popular Rust libraries!
Window
Now that we can see our window, let’s modify it to be more personal to our usage in this Bevy tutorial. We will start by customizing the window title and then add a dotted line in the middle to represent the net, similar to what you might see in the physical world.
Customizing the Window Title
To customize the window title, we need to modify the windows.rs
file. Follow these steps:
- Open the
windows.rs
file. - Make the necessary imports at the top of the file:
use bevy::prelude::*;
- Create a public function called
create_window
that returns aWindowPlugin
:
pub fn create_window() -> WindowPlugin { WindowPlugin { primary_window: Some(Window { title: "My Pong Game".to_string(), ..default() }), ..default() } }
- In the
main.rs
file, add thecreate_window
function to the default plugins:
App::new() .add_plugins(DefaultPlugins.set(create_window()))
- Ensure that the
mod.rs
file defines and uses thewindow
module:
pub mod window; pub use window::*;
When you run the command cargo run
, you should see the window load with the title “My Pong Game”.
Adding a Dotted Line
Next, let’s add a dotted line in the middle of the window to represent the net. Follow these steps:
- Define a public function called
spawn_dotted_line
in thewindows.rs
file. We’ll define a color, size, and gap for the dots on our net, as well as the number of dots we’ll use. We then loop through based on the number of dots calculated and instantiate each dot at a new position based on the size and gap:
pub fn spawn_dotted_line(mut commands: Commands) { let dot_color = Color::srgb(1.0, 1.0, 1.0); let dot_size = Vec3::new(5.0, 20.0, 1.0); let gap_size = 10.0; let num_dots = (constants::WINDOW_HEIGHT / (dot_size.y + gap_size)) as i32; for i in 0..num_dots { commands.spawn(SpriteBundle { sprite: Sprite { color: dot_color, ..default() }, transform: Transform { translation: Vec3::new(0.0, i as f32 * (dot_size.y + gap_size) - constants::WINDOW_HEIGHT / 2.0, 0.0), scale: dot_size, ..default() }, ..default() }); } }
- Ensure that the
constants.rs
file defines the window height:
pub const WINDOW_HEIGHT: f32 = 600.0;
- Import the
constants
module in thewindows.rs
file:
use crate::constants::*;
- Add the
spawn_dotted_line
function to the startup systems in themain.rs
file:
.add_systems(Startup, (spawn_dotted_line, spawn_camera))
When you run the command cargo run
, you should see the window load with a dotted line at the center of it.
As a small challenge, you can modify the size and shape of the dots. We’ll cover the solution in the next section, so pause here if you want to try it yourself!
Challenge – Change Dotted Line Color
In this last part of our Bevy tutorial, we will modify the dotted lines in our game to ensure we understand how different entities are being spawned and solve the challenge we issued. We will begin by changing the color, gap size, and dot size within the windows.rs
file.
Modifying the Dotted Lines
Let’s start by changing the color of the dots. Instead of white, we will modify the second component to be zero, making the color more pink.
let dot_color = Color::srgb(1.0, 0.0, 1.0);
Next, we will adjust the gap size. Instead of 10, we will set it to 20, making the gaps between the dots a little bit larger.
let gap_size = 20.0;
Finally, we will change the dot size. We will modify both the X and Y components to make the dots smaller.
let dot_size = Vec3::new(3.0, 15.0, 1.0);
Here is the complete modified code for the dotted lines:
pub fn spawn_dotted_line(mut commands: Commands) { let dot_color = Color::srgb(1.0, 0.0, 1.0); let dot_size = Vec3::new(3.0, 15.0, 1.0); let gap_size = 20.0; let num_dots = (constants::WINDOW_HEIGHT / (dot_size.y + gap_size)) as i32; for i in 0..num_dots { commands.spawn(SpriteBundle { sprite: Sprite { color: dot_color, ..default() }, transform: Transform { translation: Vec3::new(0.0, i as f32 * (dot_size.y + gap_size) - constants::WINDOW_HEIGHT / 2.0, 0.0), scale: dot_size, ..default() }, ..default() }); } }
When you run the game using cargo run
, you will see the changes have been made. The color is now more pink, the gaps are bigger, and the lines are thinner. This exercise demonstrates the importance of clearly defined variables and components. If we need to make any specific changes in the future, it’s easy to modify one component without affecting the others.
For the sake of cleanliness, we will revert the elements back to what we had previously. However, feel free to play around with the values to find something that suits you.
let dot_color = Color::srgb(1.0, 1.0, 1.0); let dot_size = Vec3::new(5.0, 20.0, 1.0); let gap_size = 10.0;
By understanding how to change these properties, you gain a better grasp of how entities are spawned and managed in the game.
Bevy Tutorial Wrap-Up
Congratulations on completing this Bevy tutorial! By now, you should have a solid understanding of how to develop games using Rust and Bevy. We’ve explored core concepts like ECS architecture, project structure, and Rust’s syntax, and you’ve successfully set up and built essential game components.
While this project is a great starting point, there’s so much more you can do to expand and customize your game. Consider adding new gameplay features, integrating assets like sound effects or animations, or experimenting with different mechanics to challenge yourself further. The possibilities are endless!
To continue your learning journey, check out other tutorials and courses available on Zenva that cover game development, programming, and more. For example, our Rust Programming Academy is curated learning pathway designed to teach you all things Rust with a slew of real-world projects to cement your skills!
We hope this tutorial has been informative and inspiring. Good luck with your future game development projects, and as always, happy coding!
Get industry-ready with the Rust Programming Academy! Perfectly suited to any skill level, you’ll get the tools you need to succeed while building a slew of projects!
Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it! FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.