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 (struct
s or enums
s), 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
}
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
}
}
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
}
}
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"
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::*;
To start using bevy, they provide a builder pattern to initialize the app.
main.rs
fn main() {
App::build()
/* setup goes here */
.run();
}
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();
}
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!");
}
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()
});
}
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);
}
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;
}
}
}
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)
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:
With this, the player will always be moved the right distance after a rendering update, no matter how high the framerate.
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.
Thanks. How does it know that
fn setup
is a system. Do we add#[system]
macro above it ?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.