DEV Community

Sébastien Belzile
Sébastien Belzile

Posted on • Updated on

Making Games in Rust - Part 1 - Bevy and ECS

Context

I recently stumbled upon a short YouTube video of somebody building a roguelike game in Rust.
From there, jumping from resource to resource, I ended up going through (part) of this massive (and awesome) tutorial by Herbert Wolverson about his Rust library bracket_lib. In this tutorial, Wolverson builds a roguelike game with colored text characters.
After reading through, I felt like writing another type of game in Rust, so I looked at the available Rust game engines. The most popular, seems to be Amethyst, but it looks like they halted their development efforts. Second in line was Bevy. People are using it, support for Android and iOS is on the way, uses an ECS and have some usage examples: looks good.

Now we have a language: Rust, a game engine: Bevy, and a genre: Platformer.

This series of posts will be a journal of my journey building a small platformer game with these tools.

ECS

Let's first go over the theory: what is an ECS?

An ECS, or Entity Component System is an architectural pattern used to structure games. There are 3 components to this pattern:

  • Entities: An entity identifies a game object, such as a player, a monster, a wall, etc. Technically, it acts as an identifier that points to multiple components.
  • Components: A component is the data/properties for an aspect of the game. Examples of this would be a position, a monster AI or a map. A entity is the composition of multiple components.
  • Systems: systems contain the logic of our game. They perform actions on the entities that possess the components matching a given query.

Bevy's ECS

Let's start by creating a new rust project:

cargo new platformer
Enter fullscreen mode Exit fullscreen mode
  • Open the newly created project in your favorite code editor - and navigate to the cargo.toml file.
  • Add a dependency to bevy.

In the end, your cargo.toml file should look like this:

[package]
name = "platformer"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy = "0.5"
Enter fullscreen mode Exit fullscreen mode
  • Navigate to the main.rs file
  • Replace its content by:
use bevy::prelude::*;

fn main() {
    App::build().run();
}
Enter fullscreen mode Exit fullscreen mode

use bevy::prelude::*; imports Bevy modules, and the main function starts Bevy. In the end, this code will launch Bevy and exit. It you run cargo run, nothing will happen. This is expected. We have just been through the second part of Bevy's getting started guide.

Now let's see how ECS works in Bevy.

Components

Components in Bevy are plain old Rusty struct. Ex:

struct Position { x: f32, y: f32 }
Enter fullscreen mode Exit fullscreen mode

Systems

Systems are plain old rusty functions:

fn print_position_system(query: Query<&Transform>) {
    for transform in query.iter() {
        println!("position: {:?}", transform.translation);
    }
}
Enter fullscreen mode Exit fullscreen mode

Bevy automatically converts your Rust functions to its internal System type via a trait extension method. So to register a system with Bevy, one can do:

// Declaring the system
fn hello_world() {
    println!("hello world!");
}

fn main() {
    App::build()
        .add_system(hello_world.system()) // Registering the system
        .run();
}
Enter fullscreen mode Exit fullscreen mode

Entities

Bevy's getting started guide says that it represents entities as a simple type containing a unique integer:

struct Entity(u64);
Enter fullscreen mode Exit fullscreen mode

But really, you won't really notice. To declare a new entity, one can call the Commands.spawn() method and insert multiple components as such:

fn add_people(mut commands: Commands) {
    commands.spawn().insert(Person).insert(Name("Elaina Proctor".to_string()));
    commands.spawn().insert(Person).insert(Name("Renzo Hume".to_string()));
    commands.spawn().insert(Person).insert(Name("Zayna Nieves".to_string()));
}
Enter fullscreen mode Exit fullscreen mode

You may run this code with the following components, startup method and systems:

// Components
struct Person;

struct Name(String);

// main
fn main() {
    App::build()
        .add_startup_system(add_people.system())
        .add_system(greet_people.system())
        .run();
}

// Systems
fn add_people(mut commands: Commands) {
    commands.spawn().insert(Person).insert(Name("Elaina Proctor".to_string()));
    commands.spawn().insert(Person).insert(Name("Renzo Hume".to_string()));
    commands.spawn().insert(Person).insert(Name("Zayna Nieves".to_string()));
}

fn greet_people(query: Query<&Name, With<Person>>) {
    for name in query.iter() {
        println!("hello {}!", name.0);
    }
}
Enter fullscreen mode Exit fullscreen mode

This should yield:

hello Elaina Proctor!
hello Renzo Hume!
hello Zayna Nieves!
Enter fullscreen mode Exit fullscreen mode

in your terminal.

Creating a Platformer - Step 1 - The Window

The following code will create a new 640x400 window with the title Platformer!.

fn main() {
    App::build()
        .insert_resource(WindowDescriptor {
            title: "Platformer!".to_string(),
            width: 640.0,
            height: 400.0,
            vsync: true,
            ..Default::default()
        })
        .add_plugins(DefaultPlugins)
        .run();
}
Enter fullscreen mode Exit fullscreen mode

You may have notice the line: .add_plugins(DefaultPlugins). This line adds Bevy's Default Plugins, which includes the base features expected from a game engine: 2D and 3D renderer, windows, user inputs handling, etc. This is explained in greater details on step 4 of Bevy's getting started tutorial.

Plugins are a useful part of Bevy that allow you to group together multiple resources, components, systems. More on that later.

It is also possible to modify the background color of the window:

.insert_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04)))
Enter fullscreen mode Exit fullscreen mode

Now run cargo run:

🤩🤩🤩

Image description

🤩🤩🤩

Code available here.

Discussion (3)

Collapse
adsick profile image
adsick

In new Bevy 0.6 all structs are no more Components by default, please update the tutorial.

Collapse
sbelzile profile image
Sébastien Belzile Author

To make this work with Bevy 0.6:

  • you must add #[derive(Component)] on top of your component structs to turn them into components.
  • App::build() is now App::new()
Collapse
sbelzile profile image
Sébastien Belzile Author

Some part of the tutorial will not work yet with Bevy 0.6 (part 3+, but I expect the effects to show up from part 8). There is this issue github.com/dimforge/bevy_rapier/is... remaining in Bevy Rapier that still prevents me from updating everything.