DEV Community

Cover image for The Bevy Game Engine
Ethan Tang
Ethan Tang

Posted on • Edited on

The Bevy Game Engine

NOTE: Since this has been written, Bevy has received many awesome, but sadly, breaking updates. The code in this guide is no longer maintained.

Since rust is The Best Language™, it's no surprise that it's been an attractive choice for game development. Well, when I say game development, I mean game engine development, because there are more rust game engines than actual rust games. Whenever Rust gamedev pops up in my feed, I try the new engine, follow the tutorial, and then go back to using Godot. Most of these engines sacrificed simplicity for modularity and extensibility and were generally not fun to work with.

What is Bevy?

Bevy is, in its own words, a "A refreshingly simple data-driven game engine built in Rust". It fully delivers on this promise, with possibly the least boilerplate necessary to write a game. Enough about that though, let's actually jump straight in to making! You should:

  • be familiar-ish with rust
  • have cargo installed with the latest stable rust

Writing an ECS app in Bevy

Bevy is based on ECS (Entity-Component-System) architecture, a new way of making games that is multi-threading friendly and brings a boost in performance vs standard models. It's so useful, Unity is implementing one for their engine. You can learn more on the bevy website, but here's a quick rundown.

Entities

Entities are collections of distinct components. They are represented by a single id within the bevy engine.

Components

Components can be whatever you want. They are represented by rust objects (structs or enumss), and are attached to entities.

Systems

Systems are functions. They run on collections of components. For example, the system

fn my_system(a: &ComponentA, b: &mut ComponentB) {
    //code here
}
Enter fullscreen mode Exit fullscreen mode

runs on all entities with both ComponentA and ComponentB attached. You use &mut to gain write access. This system can also be written in "query form" as

fn my_system(query: mut Query<(&ComponentA, &mut ComponentB)>) {
    for (a, b) in &mut query.iter() {
        //code here
    }
}
Enter fullscreen mode Exit fullscreen mode

This function runs only once, with Query::iter() giving you an iterator, so you can loop through it manually.

Resources

Resources are also rust objects, but they aren't associated with any Entity. Instead, they are available for all systems to use. They can be accessed like so:

fn my_system(
    resA: Res<ResourceA>, 
    resB: ResMut<ResourceB>, 
    query: mut Query<(&ComponentA, &mut ComponentB)>
) {
    for (a, b) in &mut query.iter() {
        //code here
    }
}
Enter fullscreen mode Exit fullscreen mode

Enough talk, let's get started.

Create a new project with cargo new <project name>, and open in your editor of choice.

Let's edit Cargo.toml and add bevy to our dependencies. As of writing, bevy 0.2.1 is the latest version.

Cargo.toml

[dependencies]
bevy = "0.2"
Enter fullscreen mode Exit fullscreen mode

Start by importing stuff into scope. The bevy crate is actually just a helper crate that collects all the libraries most games will need into one.

main.rs

use bevy::prelude::*;
Enter fullscreen mode Exit fullscreen mode

To start using bevy, they provide a builder pattern to initialize the app.

main.rs

fn main() {
    App::build()
        /* setup goes here */
        .run();
}
Enter fullscreen mode Exit fullscreen mode

If we run our app now, nothing happens. That's because App provides nothing by default. We use add_default_plugins() to add basic functionality.

main.rs

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

You should see a gray window pop up. Now, we need to populate our app with entities and components. For this, we use a startup system.

main.rs

fn main() {
    App::build()
        .add_default_plugins()
        .add_startup_system(setup.system())
        .run();
}

fn setup() {
    println!("setup!");
}
Enter fullscreen mode Exit fullscreen mode

Unlike regular systems, startup systems are only run once, so you should see setup! printed in your console only once.

Now, we can add a sprite. To access our ECS World, we can add arguments to the startup system that will automatically be passed by bevy.

main.rs

fn setup(
    mut commands: Commands, 
    mut materials: ResMut<Assets<ColorMaterial>>
) {
    commands
        .spawn(Camera2dComponents::default())
        .spawn(SpriteComponents {
            material: materials.add(Color::rgb(0.2, 0.2, 0.8).into()),
            transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)),
            sprite: Sprite::new(Vec2::new(32.0, 32.0)),
            ..Default::default()
        });
}
Enter fullscreen mode Exit fullscreen mode

Commands is a thread-safe buffer that will execute the commands passed to it to the ECS World.
ResMut<Assets<ColorMaterial>> is a mutable handle to the Resource Assets<ColorMaterial>, which is a collection of materials that you use to create sprites.
To display sprites, we first have to add a camera entity, which is used by the built in rendering system, and a sprite entity, which gets drawn by the rendering system.
Now you should see a small blue square in the middle of the window.

To wrap up this tutorial, let's add movement to our "game". We first must create a player marker component (a regular empty struct), and add it to the small square entity.

main.rs

struct Player;

fn setup(
    mut commands: Commands, 
    mut materials: ResMut<Assets<ColorMaterial>>
) {
    commands
        /* cut for brevity */
        .spawn(SpriteComponents /* cut for brevity */)
        .with(Player);
}
Enter fullscreen mode Exit fullscreen mode

Next, we create a new system (a regular function) that runs on all entities with Transform and Player. This system also uses the keyboard input resource, which is created in a built-in system that that's part of add_default_plugins(). Additionally, we have to register the system to the builder.

main.rs

fn main() {
    App::build()
        .add_default_plugins()
        .add_startup_system(setup.system())
        .add_system(player_movement.system())
        .run();
}

fn player_movement(
    keyboard_input: Res<Input<KeyCode>>,
    mut query: Query<(&mut Transform, &Player)>,
) {
    for (mut transform, _player) in &mut query.iter() {
        let translation = transform.translation_mut();
        if keyboard_input.pressed(KeyCode::Right) {
            *translation.x_mut() += 1.0;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, when the right arrow key is pressed, our square shifts to the right. You can complete the movement if you'd like, but I'm too lazy to do it here. You can find the completed code here:

In the next article, we'll start building a more complex game. What are your thoughts on bevy? Please do share!

Top comments (4)

Collapse
 
paulgoetze profile image
Paul Götze • Edited

Nice article, thanks!

Just a small hint: In your implementation the player movement is currently dependent on the actual framerate the game is running with. I.e. if the framerate is low, the player moves slow and vice versa.

You'd probably want to calculate the translation difference based on the time that passed, by passing a Time into the system, like:

fn player_movement(
    keyboard_input: Res<Input<KeyCode>>,
    time: Res<Time>,
    mut query: Query<(&mut Transform, &Player)>,
) {
    let v = 200.0; // velocity in units per second, v = ds / dt => ds = v * dt
    let delta = v * time.delta_seconds;

    for (mut transform, _player) in &mut query.iter() {
        let translation = transform.translation_mut();

        if keyboard_input.pressed(KeyCode::Right) {
            *translation.x_mut() += delta;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

With this, the player will always be moved the right distance after a rendering update, no matter how high the framerate.

Collapse
 
ethanyidong profile image
Ethan Tang

Yup, thanks for pointing that out! I didn't want to add too much complexity to this first example, but I'm definitely going to point this out in the next article.

Collapse
 
ajinkyax profile image
Ajinkya Borade

Thanks. How does it know that fn setup is a system. Do we add #[system] macro above it ?

Collapse
 
ethanyidong profile image
Ethan Tang

Actually, functions will automatically implement either ForEachSystem or IntoQuerySystem if its signature is correct. Since these traits are imported in prelude, we can run .system() on any valid function to get a system without macros.