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
- 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"
- Navigate to the
main.rs file
- Replace its content by:
use bevy::prelude::*;
fn main() {
App::build().run();
}
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 }
Systems
Systems are plain old rusty functions:
fn print_position_system(query: Query<&Transform>) {
for transform in query.iter() {
println!("position: {:?}", transform.translation);
}
}
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();
}
Entities
Bevy's getting started guide says that it represents entities as a simple type containing a unique integer:
struct Entity(u64);
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()));
}
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);
}
}
This should yield:
hello Elaina Proctor!
hello Renzo Hume!
hello Zayna Nieves!
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();
}
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)))
Now run cargo run
:
🤩🤩🤩
🤩🤩🤩
Code available here.
Top comments (3)
In new Bevy 0.6 all structs are no more Components by default, please update the tutorial.
To make this work with Bevy 0.6:
#[derive(Component)]
on top of your component structs to turn them into components.App::build()
is nowApp::new()
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.