Originally posted here and updated for Bevy 0.10.1.
Bevy is a data-driven game engine built in Rust. It's really straight forward to use, and a joy to work with.
In this tutorial we're going to use Bevy to make Chess, so if you've been meaning to start playing around with Bevy, this is for you!
The prerequisites for this tutorial are to have a basic-intermediate understanding of Rust, knowledge of the rules of Chess, and to be familiar with the concept of Entity Component System (ECS). If you don't know much about it, I recommend you read the Wikipedia page.
If you have any doubts, check out the Bevy book, look through the examples, or join the Official Discord to ask questions!
This tutorial was made for Bevy 0.3, but it'll be updated to new versions when they come out. The tutorial is currently up to date with version 0.4!
If you don't care about the steps, and just want to see the code, here is the repository.
Last thing before we start, showing off Bevy concepts has a higher priority than having "good code" in this tutorial, so we'll be doing some things in an awkward way that allows us to introduce more concepts, like for example using parenting in cases where it's not really needed.
As with any other Rust project, we will start by running cargo new bevy_chess
and cd bevy_chess
. This will create an empty project. The first step will be to add Bevy as a dependency by running cargo add bevy
.
A very important part of game development (as with most development) is iterating and seeing the results of our changes. Rust's long compilation times can throw a wrench on that, so as the Bevy book says, it's really recommended to enable Fast Compiles. This is an optional step, but it doesn't take too much time to set up and will help you out on the long run. To accomplish that, we need a couple things:
-
LLD Linker: The normal linker is a bit slow, so we can swap it out for the LLD Linker to get a speedup:
a. Ubuntu:
sudo apt-get install lld
b. Arch:sudo pacman -S lld
c. Windows:cargo install -f cargo-binutils
andrustup component add llvm-tools-preview
d. MacOS:brew install michaeleisel/zld/zld
-
Enable nightly Rust for this project:
rustup toolchain install nightly
to install nightly, andrustup override set nightly
on the project directory to enable it. -
Copy the contents of this file into bevy_chess/.cargo/config.
With that, fast compiles should be enabled! If you now execute cargo run, you should see all the dependencies installing. This first time will be slow, as everything needs to be downloaded, but all future compilations should be much faster. When it's done, you should see Hello, world!
as output.
Now, getting some text as output is pretty cool, but better than that is to have a window open! So let's work on that. Let's remove all the contents of main.rs
and replace them with the following:
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.run();
}
After cargo run
, you should see an empty window like the following open:
![https://caballerocoll.com/images/bevy_empty_window.png]
Let's explain what we did this step. App::new()
creates a new AppBuilder
, which has methods like add_plugins
, add_system
and add_startup_system
, which we will be using to register our Systems and Plugins. Plugins are collections of App logic and configuration, mostly used to register systems and initialize Resources. We'll get to what resources are later, and we'll also create some of our own Plugins.
With .add_plugins(DefaultPlugins)
we're adding all of the default Bevy plugins, which include things like WindowPlugin
, InputPlugin
, and TransformPlugin
. Those provide most of the features we expect from a game engine. Bevy's modularity allows us to enable only the parts that we want to use. For our game, we'll enable the defaults.
The window we opened is great, but we want to be able to change some stuff, like the title or the size. For that we will change the settings on the WindowPlugin
.
We'll change the main
function to the following:
fn main() {
App::new()
.insert_resource(Msaa::Sample4)
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Chess!".into(),
resolution: (800., 800.).into(),
..default()
}),
..default()
}))
}
The first resource we're adding, Msaa
, is for the future. It's setting up antialiasing for our game. After that we change some settings in WindowPlugin
, setting the title, width, and height. Here you can check all the other properties you can change. An important thing to note, is that this resource has to be set up before adding the default plugins, otherwise they won't work.
You should now see a bigger window with "Chess!" as the title. Everything is still empty though, so let's change that!
Now we need something to display on the screen. To achieve that, we'll create our first startup system. Startup systems are like normal systems (which we'll use later), but they only run once at the start of the game. These work great for creating entities, setting resources, or any other thing you might want to do at the start of the game.
fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>) {
// Plane
commands.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 8.0, ..default() })),
material: materials.add(Color::rgb(1., 0.9, 0.9).into()),
transform: Transform::from_translation(Vec3::new(4., 0., 4.)),
..default()
});
// Camera
commands.spawn(Camera3dBundle {
transform: Transform::from_matrix(Mat4::from_rotation_translation(
Quat::from_xyzw(-0.3, -0.5, -0.3, 0.5).normalize(),
Vec3::new(-7., 20., 4.)
)),
..default()
});
// Light
commands.spawn(PointLightBundle {
transform: Transform::from_translation(Vec3::new(4., 8., 4.)),
..default()
});
}
setup
will be a startup system, which takes commands
, and the meshes
and materials
resources. Commands is used to spawn and despawn entities, while meshes
and materials
are used to register meshes and materials.
We use commands to spawn a Plane, a Camera and a Light, by using PbrBundle
, Camera3dBundle
, and PointLightBundle
, which are Bundles. Bundles are just an easy to use collection of Components. We can override the bundle properties, like for example the transform
, which allows us to move the entities around or to change their rotation, or the mesh and material.
If you now run cargo run
, you'll see that nothing has changed! This is because we haven't registered our setup
system. Let's do that with add_startup_system
:
fn main() {
App::new()
.insert_resource(Msaa::Sample4)
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Chess!".into(),
resolution: (800., 800.).into(),
..default()
}),
..default()
}))
.add_startup_system(setup)
.run();
}
With add_startup_system
we're adding setup
as a startup system, which will only run once at the beginning of the game. If we use add_system
instead, it will run every frame.
You can now run the game to see a flat plane from a camera looking slightly down:
![https://caballerocoll.com/images/bevy_flat_plane.png]
We now have a very boring board, let's change that! We'll change the current plane to be a grid of squares of alternating colors.
For that, we'll first split the current setup
system into two:
fn main() {
App::new()
.insert_resource(Msaa::Sample4)
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Chess!".into(),
resolution: (800., 800.).into(),
..default()
}),
..default()
}))
.add_startup_system(setup)
.add_startup_system(create_board)
.run();
}
fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>) {
// Camera
commands.spawn(Camera3dBundle {
transform: Transform::from_matrix(Mat4::from_rotation_translation(
Quat::from_xyzw(-0.3, -0.5, -0.3, 0.5).normalize(),
Vec3::new(-7., 20., 4.)
)),
..default()
});
// Light
commands.spawn(PointLightBundle {
transform: Transform::from_translation(Vec3::new(4., 8., 4.)),
..default()
});
}
fn create_board(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>) {
}
Before implementing create_board
, let's take a side-trip through Asset Town. The meshes.add(Mesh::from(shape::Plane { size: 8.0, ..default() }))
line we used before registers a plane mesh to the Assets<Mesh>
resource, and returns a Handle<Mesh>
, which is what PbrBundle
uses. Bevy will then use that handle to get the actual mesh and render it.
This is great, because if we want to create multiple entities with the same Mesh
, we can just provide them the same handle and just add the Mesh once. All of this also applies to materials
and StandardMaterial
.
We will add two materials, a white and a black one, and one Plane mesh, to create a plane per square in the board.
fn create_board(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>) {
// Add meshes and materials
let mesh = meshes.add(Mesh::from(shape::Plane { size: 1., ..default() }));
let white_material = materials.add(Color::rgb(1., 0.9, 0.9).into());
let black_material = materials.add(Color::rgb(0., 0.1, 0.1).into());
// Spawn 64 squares
for i in 0..8 {
for j in 0..8 {
commands.spawn(PbrBundle {
mesh: mesh.clone(),
// Change material according to position to get alternating pattern
material: if (i + j + 1) % 2 == 0 {
white_material.clone()
} else {
black_material.clone()
},
transform: Transform::from_translation(Vec3::new(i as f32, 0., j as f32)),
..default()
});
}
}
}
We use two for loops to spawn the 64 squares, and we use if (i + j + 1) % 2 == 0
to generate the alternating pattern of colors. Here's the result:
![https://caballerocoll.com/images/bevy_chess_board.png]
Note: We could have used a single Plane with a Texture to make the pattern instead of making different squares, but doing it this way will help us later down the line, when we want to select pieces and squares for movements.
Every chess game needs some pieces to play with, so let's go ahead and get some models to play with. We'll use GLTF models, which for the most part work great in Bevy. Asset management is still a bit work in progress though, so some things might not work yet.
I got some Chess piece models from Sketchfab. Sketchfab does some weird stuff with the autoconversion to GLTF, so I downloaded the models in OBJ, the original format, and used AnyConv to convert them to GLB, which is the binary format of GLTF. You can do this yourself, or you can go over to the repo where you can download the file.
Now that we have the models, we need a way to load them onto Bevy. For that we'll use the AssetServer
resource. It provides a load()
function to which we can pass the path of the asset we want to load. In our case, we have the .glb file in assets/models/chess_kit/pieces.glb
. We can get each of the models in the file like so:
fn create_pieces(mut commands: Commands, asset_server: Res<AssetServer>, mut materials: ResMut<Assets<StandardMaterial>>) {
// Load all the meshes
let king_handle: Handle<Mesh> = asset_server.load("models/chess_kit/pieces.glb#Mesh0/Primitive0");
let king_cross_Handle: Handle<Mesh> = asset_server.load("models/chess_kit/pieces.glb#Mesh1/Primitive0");
let pawn_handle: Handle<Mesh> = asset_server.load("models/chess_kit/pieces.glb#Mesh2/Primitive0");
let knight_1_handle: Handle<Mesh> = asset_server.load("models/chess_kit/pieces.glb#Mesh3/Primitive0");
let knight_2_handle: Handle<Mesh> = asset_server.load("models/chess_kit/pieces.glb#Mesh4/Primitive0");
let rook_handle: Handle<Mesh> = asset_server.load("models/chess_kit/pieces.glb#Mesh5/Primitive0");
let bishop_handle: Handle<Mesh> = asset_server.load("models/chess_kit/pieces.glb#Mesh6/Primitive0");
let queen_handle: Handle<Mesh> = asset_server.load("models/chess_kit/pieces.glb#Mesh7/Primitive0");
// Add some materials
let white_material = materials.add(Color::rgb(1., 0.8, 0.8).into());
let black_material = materials.add(Color::rgb(0., 0.2, 0.2).into());
}
Notice that load assumes that the path you're passing is inside the assets
folder.
The #Mesh0/Primitive0
part let's us select which of the meshes we want from the GLTF file. The pieces are separated into different meshes, and some of the pieces are separated into two meshes, like the King and the Knight.
We've also added a white and a black material for the pieces, which we will now spawn. As some of the meshes have a bit of a translation, we'll use a parent entity to keep the actual position, and use a child to keep the mesh. This will also help us combine the meshes for the King and the Knight.
The best way to solve this would be to go into a 3D editing software and fix the models so each of them is at the origin and is just a single mesh, but doing it this way gives me an excuse to talk about parenting.
fn create_pieces(mut commands: Commands, asset_server: Res<AssetServer>, mut materials: ResMut<Assets<StandardMaterial>>) {
// Load all the meshes
[...]
// Add some materials
[...]
// Spawn parent entity
commands.spawn(PbrBundle {
transform: Transform::from_translation(Vec3::new(0., 0., 4.)),
..default()
})
// Add children to parent
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: king_handle.clone(),
material: white_material.clone(),
transform: Transform::from_translation(Vec3::new(-0.2, 0., -1.9)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
parent.spawn(PbrBundle {
mesh: king_cross_handle.clone(),
material: white_material.clone(),
transform: Transform::from_translation(Vec3::new(-0.2, 0., -1.9)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
});
}
Here we used the with_children()
function to add two children to a parent entity. The function takes a closure, that in turn takes a parent
parameter, which is similar to commands
, and let's us spawn children. This two children are moved with respect to their parent, to compensate for the translation that the model has, and with scaling added to make them fit in a square.
Don't forget to add create_pieces
as a startup system! If you run the game now you should see a single white King on it's square:
![https://caballerocoll.com/images/bevy_chess_king.png]
Great, we got pieces on our board! But if you check the previous code, it's more than 20 lines just to spawn a piece. This function is going to get really busy really soon if we don't break it up. Let's create a new pieces.rs
file. We'll make separate functions to spawn each of the pieces:
use bevy::prelude::*;
pub fn spawn_king(commands: &mut Commands, material: Handle<StandardMaterial>, mesh: Handle<Mesh>, mesh_cross: Handle<Mesh>, position: Vec3) {
commands.spawn(PbrBundle {
transform: Transform::from_translation(position),
..default()
})
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: mesh,
material: material.clone(),
transform: Transform::from_translation(Vec3::new(-0.2, 0., -1.9)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
parent.spawn(PbrBundle {
mesh: mesh_cross,
material: material,
transform: Transform::from_translation(Vec3::new(-0.2, 0., -1.9)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
});
}
pub fn spawn_knight(commands: &mut Commands, material: Handle<StandardMaterial>, mesh_1: Handle<Mesh>, mesh_2: Handle<Mesh>, position: Vec3) {
commands.spawn(PbrBundle {
transform: Transform::from_translation(position),
..default()
})
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: mesh_1,
material: material.clone(),
transform: Transform::from_translation(Vec3::new(-0.2, 0., 0.9)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
parent.spawn(PbrBundle {
mesh: mesh_2,
material: material,
transform: Transform::from_translation(Vec3::new(-0.2, 0., 0.9)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
});
}
pub fn spawn_queen(commands: &mut Commands, material: Handle<StandardMaterial>, mesh: Handle<Mesh>, position: Vec3) {
commands.spawn(PbrBundle {
transform: Transform::from_translation(position),
..default()
})
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: mesh,
material: material,
transform: Transform::from_translation(Vec3::new(-0.2, 0., -0.95)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
});
}
pub fn spawn_bishop(commands: &mut Commands, material: Handle<StandardMaterial>, mesh: Handle<Mesh>, position: Vec3) {
commands.spawn(PbrBundle {
transform: Transform::from_translation(position),
..default()
})
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: mesh,
material: material,
transform: Transform::from_translation(Vec3::new(-0.1, 0., 0.)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
});
}
pub fn spawn_rook(commands: &mut Commands, material: Handle<StandardMaterial>, mesh: Handle<Mesh>, position: Vec3) {
commands.spawn(PbrBundle {
transform: Transform::from_translation(position),
..default()
})
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: mesh,
material: material,
transform: Transform::from_translation(Vec3::new(-0.1, 0., 1.8)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
});
}
pub fn spawn_pawn(commands: &mut Commands, material: Handle<StandardMaterial>, mesh: Handle<Mesh>, position: Vec3) {
commands.spawn(PbrBundle {
transform: Transform::from_translation(position),
..default()
})
.with_children(|parent| {
parent.spawn(PbrBundle {
mesh: mesh,
material: material,
transform: Transform::from_translation(Vec3::new(-0.2, 0., 2.6)).with_scale(Vec3::new(0.2, 0.2, 0.2)),
..default()
});
});
}
fn create_pieces(mut commands: Commands, asset_server: Res<AssetServer>, mut materials: ResMut<Assets<StandardMaterial>>) {
// Load all the meshes
[...]
// Add some materials
[...]
spawn_rook(&mut commands, white_material.clone(), rook_handle.clone(), Vec3::new(0., 0., 0.));
spawn_knight(&mut commands, white_material.clone(), knight_1_handle.clone(), knight_2_handle.clone(), Vec3::new(0., 0., 1.));
spawn_bishop(&mut commands, white_material.clone(), bishop_handle.clone(), Vec3::new(0., 0., 2.));
spawn_queen(&mut commands, white_material.clone(), queen_handle.clone(), Vec3::new(0., 0., 3.));
spawn_king(&mut commands, white_material.clone(), king_handle.clone(), king_cross_handle.clone(), Vec3::new(0., 0., 4.));
spawn_bishop(&mut commands, white_material.clone(), bishop_handle.clone(), Vec3::new(0., 0., 5.));
spawn_knight(&mut commands, white_material.clone(), knight_1_handle.clone(), knight_2_handle.clone(), Vec3::new(0., 0., 6.));
spawn_rook(&mut commands, white_material.clone(), rook_handle.clone(), Vec3::new(0., 0., 7.));
for i in 0..8 {
spawn_pawn(&mut commands, white_material.clone(), pawn_handle.clone(), Vec3::new(1., 0., i as f32));
}
spawn_rook(&mut commands, black_material.clone(), rook_handle.clone(), Vec3::new(7., 0., 0.));
spawn_knight(&mut commands, black_material.clone(), knight_1_handle.clone(), knight_2_handle.clone(), Vec3::new(7., 0., 1.));
spawn_bishop(&mut commands, black_material.clone(), bishop_handle.clone(), Vec3::new(7., 0., 2.));
spawn_queen(&mut commands, black_material.clone(), queen_handle.clone(), Vec3::new(7., 0., 3.));
spawn_king(&mut commands, black_material.clone(), king_handle.clone(), king_cross_handle.clone(), Vec3::new(7., 0., 4.));
spawn_bishop(&mut commands, black_material.clone(), bishop_handle.clone(), Vec3::new(7., 0., 5.));
spawn_knight(&mut commands, black_material.clone(), knight_1_handle.clone(), knight_2_handle.clone(), Vec3::new(7., 0., 6.));
spawn_rook(&mut commands, black_material.clone(), rook_handle.clone(), Vec3::new(7., 0., 7.));
for i in 0..8 {
spawn_pawn(&mut commands, black_material.clone(), pawn_handle.clone(), Vec3::new(6., 0., i as f32));
}
}
That's a lot, but with that out of the way, we now have all of our pieces on the board:
![https://caballerocoll.com/images/bevy_chess_all_pieces.png]
Cool, our board and our pieces are all set up, but we still need to implement all of the game logic.
The next step we're going to work in is selecting pieces. For that, we'll use the bevy_mod_picking library. It provides a simple API to select meshes in your game.
To use it first we need to follow the short instructions shown in the README. We'll run cargo add bevy_mod_picking
, then import the library and register the DefaultPickingPlugins
it provides:
mod pieces;
use pieces::*;
use bevy::prelude::*;
use bevy_mod_picking::prelude::*;
fn main() {
App::new()
.insert_resource(Msaa::Sample4)
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Chess!".into(),
resolution: (800., 800.).into(),
..default()
}),
..default()
}))
.add_plugins(DefaultPickingPlugins
.build()
.disable::<DebugPickingPlugin>()
)
.add_startup_system(setup)
.add_startup_system(create_board)
.add_startup_system(create_pieces)
.run();
}
Next we need to add a component to the Camera object, to mark it as a pick source. For that, we'll use the with()
function, which adds components to the previous Entity:
fn setup(mut commands: Commands) {
// Camera
commands.spawn((Camera3dBundle {
transform: Transform::from_matrix(Mat4::from_rotation_translation(
Quat::from_xyzw(-0.3, -0.5, -0.3, 0.5).normalize(),
Vec3::new(-7., 20., 4.)
)),
..default()
}, RaycastPickCamera::default()));
// Light
commands.spawn(PointLightBundle {
transform: Transform::from_translation(Vec3::new(4., 8., 4.)),
..default()
});
}
This way, our Camera entity will be spawned with all of the components in the Camera3dBundle
bundle and the RaycastPickCamera
component, and nothing will change for the PointLightBundle
spawned under it.
We now just need to add the PickableBundle
and RaycastPickTarget
bundles to the entities we want to be able to select. In our case, we'll add it to the board squares:
fn create_board(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>) {
[...]
commands.spawn((PbrBundle {
mesh: mesh.clone(),
// Change material according to position to get alternating pattern
material: if (i + j + 1) % 2 == 0 {
white_material.clone()
} else {
black_material.clone()
},
transform: Transform::from_translation(Vec3::new(i as f32, 0., j as f32)),
..default()
}, PickableBundle::default(), RaycastPickableBundle::default()));
[...]
}
If you now run the game, you should see a small green ball that follows your mouse all over the board, cool! Feel free to remove the DebugPickingPlugin
when you want, it's just an easy way to check that everything is working correctly.
We should implement something to select squares, but first, notice that our main.rs
file has been growing a lot, so it would be great to split it up a bit.
We'll do a couple quick adjustments to keep our code a bit cleaner. First we'll move create_pieces
into pieces.rs
and make it public, and we'll change all of the spawn_[piece]
functions to private.
Next we'll create a new board.rs
file and we'll move create_board
there and make it public. We now just have to import it in main.rs
, and we're ready to go.
We're going to create our very own component now, so exciting! Let's go to board.rs
and add the following:
use bevy::prelude::*;
#[derive(Component)]
pub struct Square {
pub x: u8,
pub y: u8
}
That's it, that's our component. No boilerplate needed. We can now just add this to the squares like this:
fn create_board(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>) {
[...]
commands.spawn((PbrBundle {
mesh: mesh.clone(),
// Change material according to position to get alternating pattern
material: if (i + j + 1) % 2 == 0 {
white_material.clone()
} else {
black_material.clone()
},
transform: Transform::from_translation(Vec3::new(i as f32, 0., j as f32)),
..default()
}, PickableBundle::default(),
RaycastPickTarget::default(),
Square {
x: i,
y: j
}));
[...]
}
Square
is a normal Rust struct, so we can do something like the following to add some functions which we can later use on the component:
impl Square {
fn is_white(&self) -> bool {
(self.x + self.y + 1) % 2 == 0
}
}
We'll make a small change to help with selecting squares. In the way we're creating squares now, we are adding only two materials, and cloning the handles into each square. We'll change it to use one material per square, this way we can change a square's color individually. It's an easy change:
fn create_board(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>) {
// Add meshes and materials
let mesh = meshes.add(Mesh::from(shape::Plane { size: 1., ..default() }));
// Spawn 64 squares
for i in 0..8 {
for j in 0..8 {
commands.spawn((PbrBundle {
mesh: mesh.clone(),
// Change material according to position to get alternating pattern
material: if (i + j + 1) % 2 == 0 {
materials.add(Color::rgb(1., 0.9, 0.9).into())
} else {
materials.add(Color::rgb(0., 0.1, 0.1).into())
},
transform: Transform::from_translation(Vec3::new(i as f32, 0., j as f32)),
..default()
}, PickableBundle::default(),
RaycastPickTarget::default(),
Square {
x: i,
y: j
},
OnPointer::<Click>::run_callback(|In(event): In<ListenedEvent<Click>>, mut selected_square: ResMut<SelectedSquare>| {
selected_square.entity = Some(event.target);
Bubble::Up
}),
OnPointer::<Over>::run_callback(|In(event): In<ListenedEvent<Over>>, mut hover_square: ResMut<HoverSquare>| {
hover_square.entity = Some(event.target);
Bubble::Up
})));
}
}
}
Note: In theory this should work by just creating 4 materials and replacing the handles in the squares, but there's a bug in Bevy that makes that not work. Update: This has now been fixed! In the last section we'll rework this to use the slightly more efficient version. Update: This has been reworked a bit to simply just pass the target of the event (whether it be the mouse hovering over an entity such as with OnPointer::<Over>
or the mouse clicking on an entity as with OnPointer::<Click>
).
Everything should look exactly the same. Let's create a resource to keep track of which square is currently selected as well as which square is being hovered over:
#[derive(Default, Resource)]
struct SelectedSquare {
entity: Option<Entity>
}
#[derive(Default, Resource)]
struct HoverSquare {
entity: Option<Entity>
}
Easy as that! We're deriving Default
so that when the plugin is initialized it starts with a None
value, but we could provide an initial value in case we wanted to, by implementing FromResources
. You can see how to do that with this Bevy example.
We'll now make a system that changes the color of the squares:
fn color_squares(selected_square: Res<SelectedSquare>, hover_square: Res<HoverSquare>, mut materials: ResMut<Assets<StandardMaterial>>, query: Query<(Entity, &Square, &Handle<StandardMaterial>)>) {
for (entity, square, material_handle) in query.iter() {
let material = materials.get_mut(material_handle).unwrap();
material.base_color = if Some(entity) == hover_square.entity {
Color::rgb(0.8, 0.3, 0.3)
} else if Some(entity) == selected_square.entity {
Color::rgb(0.9, 0.1, 0.1)
} else if square.is_white() {
Color::rgb(1., 0.9, 0.9)
} else {
Color::rgb(0., 0.1, 0.1)
};
}
}
There's a lot of new stuff here, so let's unpack that. The system first gets the entity under the cursor using our HoverSquare
resource, which gives us the entity.
Query
is a tad more interesting, it provides us with an iterable of all the entities that have the Components we select, in this case Entity
(which all entities have), Square
, and Handle<StandardMaterial>
. We can now iterate over it with query.iter()
, which will provide us access to the components.
Note: In queries, components have to be references (i.e. have &
), except Entity
, which is used normally.
For each of the squares, we first get the actual material from the Assets<StandardMaterial>
resource using get_mut()
, and then we set the base color according to if it's hovered, selected, white or black, in that order. This way hovered squares get painted the hovered color even if they're selected.
Feel free to play around with the colors and change them to something else!
We still need a way to select squares, but before we do that, we're going to create our a plugin to keep things a bit more clean. In board.rs
we're going to add the following, and change all of the systems to private:
pub struct BoardPlugin;
impl Plugin for BoardPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SelectedSquare>()
.init_resource::<HoverSquare>()
.add_startup_system(create_board)
.add_system(color_squares);
}
}
init_resource
is what takes care of initializing SelectedSquare
and HoverSquare
. If we don't add this, Bevy will complain when we try to use the resource. Now in main.rs
we can add BoardPlugin
like DefaultPlugins
or DefaultPickingPlugins
:
fn main() {
App::new()
.insert_resource(Msaa::Sample4)
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Chess!".into(),
resolution: (800., 800.).into(),
..default()
}),
..default()
}))
.add_plugins(DefaultPickingPlugins
.build()
.disable::<DebugPickingPlugin>()
)
.add_plugin(BoardPlugin)
.add_startup_system(setup)
.add_startup_system(create_pieces)
.run();
}
This way we don't have to keep track of which board systems exist from main.rs
, and everything is self contained in board.rs
.
If you run the game now, you should see the square under the cursor being highlighted by the color we selected, and when we click on the square, it should highlight with the other color we selected.
Currently, our pieces are just an empty parent object with a child that holds the mesh, and we have no way to distinguish between them. Let's create a Piece
component to keep what piece it is.
#[derive(Clone, Copy, PartialEq)]
pub enum PieceColor {
White,
Black
}
#[derive(Clone, Copy, PartialEq)]
pub enum PieceType {
King,
Queen,
Bishop,
Knight,
Rook,
Pawn
}
#[derive(Clone, Copy, Component)]
pub struct Piece {
pub color: PieceColor,
pub piece_type: PieceType,
// Current Position
pub x: u8,
pub y: u8
}
We first add two enums with all the possibilities, and then declare the Piece
component, which will have a PieceColor
, PieceType
, and the piece position. The reason that we have the position here again, and we don't just use the Transform
component, is that we will want the pieces to move from one square to the next, and there will be times when the piece is halfway between two squares, and we want to know the one it's actually supposed to be on.
We're also going to change how we call the spawn_[piece]
function calls to be like the following:
spawn_rook(commands, white_material.clone(), PieceColor::White, rook_handle.clone(), (0, 0));
And the definitions to be like:
pub fn spawn_rook(commands: &mut Commands, material: Handle<StandardMaterial>, piece_color: PieceColor, mesh: Handle<Mesh>, position: (u8, u8)) {
commands.spawn((PbrBundle {
transform: Transform::from_translation(Vec3::new(position.0 as f32, 0., position.1 as f32)),
..default()
},
Piece {
color: piece_color,
piece_type: PieceType::Rook,
x: position.0,
y: position.1
}))
.with_children(|parent| {
[...]
});
}
Great, now we spawn the pieces with the correct values set in the Pieces component. We should now be ready to implement movement.
We're going to create a system that takes care of moving the pieces to the x, y
coordinates on the Piece
component. This way we just have to change those values, and the piece will start moving to it's new coordinates. This is pretty straight-forward:
fn move_pieces(time: Res<Time>, mut query: Query<(&mut Transform, &Piece)>) {
for (mut transform, piece) in query.iter_mut() {
// Get the direction to move in
let direction = Vec3::new(piece.x as f32, 0., piece.y as f32) - transform.translation;
// Only move if the piece isn't already there (distance is big)
if direction.length() > 0.1 {
transform.translation += direction.normalize() * time.delta_seconds();
}
}
}
We'll also make a PiecesPlugin
, very similar to the BoardPlugin
:
pub struct PiecesPlugin;
impl Plugin for PiecesPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(create_pieces)
.add_system(move_pieces);
}
}
Remember to add this plugin in main.rs
, and change create_pieces
to private!
We now need to implement something to change the unit positions. We'll change the callback for the Click
event to also move the pieces for now, and we'll refactor it into something neater later:
#[derive(Default, Resource)]
struct SelectedPiece {
entity: Option<Entity>
}
fn create_board(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>) {
[...]
OnPointer::<Click>::run_callback(|In(event): In<ListenedEvent<Click>>, mut selected_square: ResMut<SelectedSquare>, mut selected_piece: ResMut<SelectedPiece>, squares_query: Query<&Square>, mut pieces_query: Query<(Entity, &mut Piece)>| {
if let Ok(square) = squares_query.get(event.target) {
selected_square.entity = Some(event.target);
if let Some(selected_piece_entity) = selected_piece.entity {
if let Ok((_piece_entity, mut piece)) = pieces_query.get_mut(selected_piece_entity) {
piece.x = square.x;
piece.y = square.y;
}
selected_square.entity = None;
selected_piece.entity = None;
} else {
for (piece_entity, piece) in pieces_query.iter_mut() {
if piece.x == square.x && piece.y == square.y {
selected_piece.entity = Some(piece_entity);
break;
}
}
}
}
Bubble::Up
})
[...]
}
We first added a new SelectedPiece
resource, which will work like SelectedSquare
, keeping track of the currently selected piece. If there is a selected piece and the player clicks another square, we want the piece to move there and then deselect the piece. If there is no piece selected, and the user clicks a square, we want to select the piece on that square, if there is one. If the player clicked outside of the board, we deselect everything. For that last part (the deselecting), I can't get it to work with bevy_mod_picking
, so if anyone has any idea of how to get it to work, please feel free to submit a PR with the relevant code and I'll update this part of the tutorial.
This code is a bit confusing, so we'll rework it later. For now, it does what we want! Look at those pieces moving:
![https://caballerocoll.com/images/bevy_chess_pieces_moving.gif]
We now need to limit what squares a piece can move to, because currently every piece can move anywhere. There's two approaches here. One of them is computing which squares are available and allowed for that piece, and checking if the move is one of those squares, and the other approach is to check that the move is a valid square (allowed and available). The first one is a bit more complicated, but it allows us to highlight all of the available squares, while the second is easier to do. (Actually the first one can be done with the second, just checking all 64 squares). We'll go with the second one, but feel free to implement the first one on your own!
Now, we'll add some helper functions first to make our work easier implementing validation. The first function we'll add just returns the color of a square if there is a piece, and returns None
if it's empty:
fn color_of_square(pos: (u8, u8), pieces: &Vec<Piece>) -> Option<PieceColor> {
for piece in pieces {
if piece.x == pos.0 && piece.y == pos.1 {
return Some(piece.color);
}
}
None
}
This way we can make test for things like not moving into a piece of the same color, or allowing diagonal movement for pawns only when there's a piece of the opposite color. Let's also add a function to check if all the squares between two are empty. This will serve us for the movement of the Rook, Bishop and Queen:
fn is_path_empty(begin: (u8, u8), end: (u8, u8), pieces: &Vec<Piece>) -> bool {
// Same column
if begin.0 == end.0 {
for piece in pieces {
if piece.x == begin.0 && ((piece.y > begin.1 && piece.y < end.1) || (piece.y > end.1 && piece.y < begin.1)) {
return false;
}
}
}
// Same row
if begin.1 == end.1 {
for piece in pieces {
if piece.y == begin.1 && ((piece.x > begin.0 && piece.x < end.0) || (piece.x > end.0 && piece.x < begin.0)) {
return false;
}
}
}
// Diagonals
let x_diff = (begin.0 as i8 - end.0 as i8).abs();
let y_diff = (begin.1 as i8 - end.1 as i8).abs();
if x_diff == y_diff {
for i in 1..x_diff {
let pos = if begin.0 < end.0 && begin.1 < end.1 {
// left bottom - right top
(begin.0 + i as u8, begin.1 + i as u8)
} else if begin.0 < end.0 && begin.1 > end.1 {
// left top - right bottom
(begin.0 + i as u8, begin.1 - i as u8)
} else if begin.0 > end.0 && begin.1 < end.1 {
// right bottom - left top
(begin.0 - i as u8, begin.1 + i as u8)
} else {
// begin.0 > end.0 && begin.1 > end.1
// right top - left bottom
(begin.0 - i as u8, begin.1 - i as u8)
};
if color_of_square(pos, pieces).is_some() {
return false;
}
}
}
true
}
The way we implemented it works both in straight lines and in diagonals, but we could have split it into two separate functions if we wanted to.
The function first checks for a path in the same column, then in the same row, and then in the four diagonals. We can now implement a method to check if a move is valid for a certain Piece
. We'll count taking a piece as a valid move, and we'll deal with the actual taking of the piece later. This function is a bit long, but here it is in it's entirety:
impl Piece {
pub fn is_move_valid(&self, new_position: (u8, u8), pieces: Vec<Piece>) -> bool {
// If there's a piece of the same color in the same square, it can't move
if color_of_square(new_position, &pieces) == Some(self.color) {
return false;
}
match self.piece_type {
PieceType::King => {
// Horizontal
((self.x as i8 - new_position.0 as i8).abs() == 1 && (self.y == new_position.1))
// Vertical
|| ((self.y as i8 - new_position.1 as i8).abs() == 1 && (self.x == new_position.0))
// Diagnoal
|| ((self.x as i8 - new_position.0 as i8).abs() == 1 && (self.y as i8 - new_position.1 as i8).abs() == 1)
},
PieceType::Queen => {
is_path_empty((self.x, self.y), new_position, &pieces)
&& ((self.x as i8 - new_position.0 as i8).abs() == (self.y as i8 - new_position.1 as i8).abs()
|| ((self.x == new_position.0 && self.y != new_position.1)
|| (self.y == new_position.1 && self.x != new_position.0)))
},
PieceType::Bishop => {
is_path_empty((self.x, self.y), new_position, &pieces)
&& (self.x as i8 - new_position.0 as i8).abs() == (self.y as i8 - new_position.1 as i8).abs()
},
PieceType::Knight => {
((self.x as i8 - new_position.0 as i8).abs() == 2
&& (self.y as i8 - new_position.1 as i8).abs() == 1)
|| ((self.x as i8 - new_position.0 as i8).abs() == 1
&& (self.y as i8 - new_position.1 as i8).abs() == 2)
},
PieceType::Rook => {
is_path_empty((self.x, self.y), new_position, &pieces)
&& ((self.x == new_position.0 && self.y != new_position.1)
|| (self.y == new_position.1 && self.x != new_position.0))
},
PieceType::Pawn => {
if self.color == PieceColor::White {
// Normal move
if new_position.0 as i8 - self.x as i8 == 1 && (self.y == new_position.1) {
if color_of_square(new_position, &pieces).is_none() {
return true;
}
}
// Move 2 sqauares
if self.x == 1 && new_position.0 as i8 - self.x as i8 == 2 && (self.y == new_position.1) && is_path_empty((self.x, self.y), new_position, &pieces) {
if color_of_square(new_position, &pieces).is_none() {
return true;
}
}
// Take piece
if new_position.0 as i8 - self.x as i8 == 1 && (self.y as i8 - new_position.1 as i8).abs() == 1 {
if color_of_square(new_position, &pieces) == Some(PieceColor::Black) {
return true;
}
}
} else {
// Normal move
if new_position.0 as i8 - self.x as i8 == -1 && (self.y == new_position.1) {
if color_of_square(new_position, &pieces).is_none() {
return true;
}
}
// Move 2 sqauares
if self.x == 6 && new_position.0 as i8 - self.x as i8 == -2 && (self.y == new_position.1) && is_path_empty((self.x, self.y), new_position, &pieces) {
if color_of_square(new_position, &pieces).is_none() {
return true;
}
}
// Take piece
if new_position.0 as i8 - self.x as i8 == -1 && (self.y as i8 - new_position.1 as i8).abs() == 1 {
if color_of_square(new_position, &pieces) == Some(PieceColor::White) {
return true;
}
}
}
false
}
}
}
}
We first check that there is no piece of the same color there, and then we match on the type of piece, because each has a different movement and needs different logic.
Most pieces are relatively simple to check. For example, for the King we check three possibilities: same row and a movement of 1 vertically, same column and movement of 1 horizontally, or a diagonal movement (movement of 1 horizontally and movement of 1 vertically). With the Queen, Bishop, and Rook we also check that the path is empty.
Pawns are a bit more interesting. As they can only move in one direction, we separate the two colors, so that white can only go up and black can only go down. The movement is then split into three different possibilities: move forward one square, move two squares if it's on the initial square, and taking a piece in the diagonals.
We won't implement en passant or castling, although those shouldn't take too much time if you want to figure them out.
Finally, we just need to call the function before moving a piece to check that the move is valid:
fn create_board(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>) {
[...]
let pieces_vec = pieces_query.iter_mut().map(|(_, piece)| * piece).collect();
if let Ok((_piece_entity, mut piece)) = pieces_query.get_mut(selected_piece_entity) {
if piece.is_move_valid((square.x, square.y), pieces_vec) {
piece.x = square.x;
piece.y = square.y;
}
}
[...]
}
Cool, now pieces can only do legal moves! But if you notice, when we move to a piece occupied by a piece of a different color, nothing happens and both pieces stand in the same place. Let' s solve that by implementing taking pieces.
This next chapter will be lighter than the past one, we just have to check the landing square for pieces of the opposite color, and despawn them if there are.
For that we first need to add Commands
as a parameter to our closure as part of the OnPointer::<Click>
event listener:
|In(event): In<ListenedEvent<Click>>, mut entity_commands: Commands, mut selected_square: ResMut<SelectedSquare>, mut selected_piece: ResMut<SelectedPiece>, squares_query: Query<&Square>, mut pieces_query: Query<(Entity, &mut Piece, &Children)>|
Commands
has the entity
function which takes an Entity
, and returns EntityCommands
. Through that, we can use the despawn
function to despawn the entity.
A note about Commands, Resources, and Queries: they have to be in this order, otherwise the system won't work. If you try putting Resources before Commands, or Queries before any of the other two, Bevy will complain.
Here's the new function that also takes care of despawning a piece after it's taken:
OnPointer::<Click>::run_callback(|In(event): In<ListenedEvent<Click>>, mut entity_commands: Commands, mut selected_square: ResMut<SelectedSquare>, mut selected_piece: ResMut<SelectedPiece>, squares_query: Query<&Square>, mut pieces_query: Query<(Entity, &mut Piece, &Children)>| {
if let Ok(square) = squares_query.get(event.target) {
selected_square.entity = Some(event.target);
if let Some(selected_piece_entity) = selected_piece.entity {
let pieces_entity_vec: Vec<(Entity, Piece, Vec<Entity>)> = pieces_query.iter_mut().map(|(entity, piece, children)| {
(
entity,
*piece,
children.iter().map(|entity| *entity).collect()
)
}).collect();
let pieces_vec = pieces_query.iter_mut().map(|(_, piece, _)| * piece).collect();
if let Ok((_piece_entity, mut piece, _piece_children)) = pieces_query.get_mut(selected_piece_entity) {
if piece.is_move_valid((square.x, square.y), pieces_vec) {
for (other_entity, other_piece, _other_children) in pieces_entity_vec {
if other_piece.x == square.x && other_piece.y == square.y && other_piece.color != piece.color {
// Despawn piece
entity_commands.entity(other_entity).despawn_recursive();
}
}
// Move piece
piece.x = square.x;
piece.y = square.y;
}
}
selected_square.entity = None;
selected_piece.entity = None;
} else {
for (piece_entity, piece, _) in pieces_query.iter_mut() {
if piece.x == square.x && piece.y == square.y {
selected_piece.entity = Some(piece_entity);
break;
}
}
}
}
Bubble::Up
})
The first difference you'll see is that we added &Children
to the Query. The despawn
function won't despawn child entities automatically, so in a normal game we'd use despawn_recursive
, which does, but this is a nice way to show how to access children entities from a system. When we refactor this function later we'll change it despawn_recursive
.
Children
is a vector of entities, so we can iterate over it and despawn the child entities.
We also added pieces_entity_vec
, so the borrow checker doesn't complain about having borrowed pieces_query
twice. Finally, we added a loop to check if there is any piece of the opposite color in that square, and if there is, we despawn it and it's children.
If you run the game now, you can take a piece and it will be despawned, cool!
The last two things we need to do are turns, and ending the game when the King is taken. We won't implement checking for mates or anything else, but it also shouldn't be hard if you want to add it to is_move_valid
.
For turns we'll create a resource that contains who is the next player to move, and we'll check if the selected piece is of that color. After moving, we'll switch the resource to the opposite color.
#[derive(Resource)]
struct PlayerTurn(PieceColor);
impl Default for PlayerTurn {
fn default() -> Self {
Self(PieceColor::White)
}
}
pub struct BoardPlugin;
impl Plugin for BoardPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SelectedSquare>()
.init_resource::<HoverSquare>()
.init_resource::<SelectedPiece>()
.init_resource::<PlayerTurn>()
.add_startup_system(create_board)
.add_system(color_squares);
}
}
We can now change the OnPointer::<Click>
closure to check if the selected piece is of the correct color before selecting it:
OnPointer::<Click>::run_callback(|In(event): In<ListenedEvent<Click>>, mut entity_commands: Commands, mut selected_square: ResMut<SelectedSquare>, mut selected_piece: ResMut<SelectedPiece>, mut turn: ResMut<PlayerTurn>, squares_query: Query<&Square>, mut pieces_query: Query<(Entity, &mut Piece, &Children)>| {
if let Ok(square) = squares_query.get(event.target) {
selected_square.entity = Some(event.target);
if let Some(selected_piece_entity) = selected_piece.entity {
let pieces_entity_vec: Vec<(Entity, Piece, Vec<Entity>)> = pieces_query.iter_mut().map(|(entity, piece, children)| {
(
entity,
*piece,
children.iter().map(|entity| *entity).collect()
)
}).collect();
let pieces_vec = pieces_query.iter_mut().map(|(_, piece, _)| * piece).collect();
if let Ok((_piece_entity, mut piece, _piece_children)) = pieces_query.get_mut(selected_piece_entity) {
if piece.is_move_valid((square.x, square.y), pieces_vec) {
for (other_entity, other_piece, _other_children) in pieces_entity_vec {
if other_piece.x == square.x && other_piece.y == square.y && other_piece.color != piece.color {
// Despawn piece
entity_commands.entity(other_entity).despawn_recursive();
}
}
// Move piece
piece.x = square.x;
piece.y = square.y;
turn.0 = match turn.0 {
PieceColor::White => PieceColor::Black,
PieceColor::Black => PieceColor::White
};
}
}
selected_square.entity = None;
selected_piece.entity = None;
} else {
for (piece_entity, piece, _) in pieces_query.iter_mut() {
if piece.x == square.x && piece.y == square.y && piece.color == turn.0 {
selected_piece.entity = Some(piece_entity);
break;
}
}
}
}
Bubble::Up
})
And with that, turns are done! If you play the game and try to move a Black piece at the start it won't work, but after moving a White you'll be able to.
We just need to end the game when the King is taken, and we'll be done with out minimal chess.
We have to choose what to do when the King is taken. For this tutorial, we'll just exit the game and print the result to the console, but you can change it later to do something else, like start a new game, or maybe show some text on screen displaying the winner.
Exiting the game involves sending Events. We just need to send the AppExit
event to Events<AppExit>
, using the send
method it provides.
OnPointer::<Click>::run_callback(|In(event): In<ListenedEvent<Click>>, mut entity_commands: Commands, mut selected_square: ResMut<SelectedSquare>, mut selected_piece: ResMut<SelectedPiece>, mut turn: ResMut<PlayerTurn>, mut app_exit_events: ResMut<Events<AppExit>>, squares_query: Query<&Square>, mut pieces_query: Query<(Entity, &mut Piece, &Children)>| {
if let Ok(square) = squares_query.get(event.target) {
selected_square.entity = Some(event.target);
if let Some(selected_piece_entity) = selected_piece.entity {
let pieces_entity_vec: Vec<(Entity, Piece, Vec<Entity>)> = pieces_query.iter_mut().map(|(entity, piece, children)| {
(
entity,
*piece,
children.iter().map(|entity| *entity).collect()
)
}).collect();
let pieces_vec = pieces_query.iter_mut().map(|(_, piece, _)| * piece).collect();
if let Ok((_piece_entity, mut piece, _piece_children)) = pieces_query.get_mut(selected_piece_entity) {
if piece.is_move_valid((square.x, square.y), pieces_vec) {
for (other_entity, other_piece, _other_children) in pieces_entity_vec {
if other_piece.x == square.x && other_piece.y == square.y && other_piece.color != piece.color {
if other_piece.piece_type == PieceType::King {
// If the king is taken, we should exit
println!("{} won! Thanks for playing!", match turn.0 {
PieceColor::White => "White",
PieceColor::Black => "Black"
});
app_exit_events.send(AppExit);
}
// Despawn pice
entity_commands.entity(other_entity).despawn_recursive();
}
}
// Move piece
piece.x = square.x;
piece.y = square.y;
turn.0 = match turn.0 {
PieceColor::White => PieceColor::Black,
PieceColor::Black => PieceColor::White
};
}
}
selected_square.entity = None;
selected_piece.entity = None;
} else {
for (piece_entity, piece, _) in pieces_query.iter_mut() {
if piece.x == square.x && piece.y == square.y && piece.color == turn.0 {
selected_piece.entity = Some(piece_entity);
break;
}
}
}
}
Bubble::Up
})
AppExit
isn't included in bevy::prelude
, so we need to change the use statement to include that. Then when we take a piece, we check it it's the King, and if it is we print a statement to the console and send the event. That's it, our game finishes when we take the King!
![https://caballerocoll.com/images/bevy_chess_finished.gif]
The main game logic is all done now, but we'll make some other changes to top it all off.
One topic we haven't delved into is UI. We'll do something simple, to show the basics of how it works, like showing which player should move next.
We'll create a new file called ui.rs
, and we'll add two systems to it: init_next_move_text
and next_move_text_update
. The first one will be a startup system, which will spawn a CameraUiBundle
and the text, and the second one will take care of updating the text when the turn changes. We'll also make a plugin, to keep everything more organized. Here's the whole file:
use crate::{board::*, pieces::*};
use bevy::{prelude::*, core_pipeline::clear_color::ClearColorConfig};
// Component to mark the Text entity
#[derive(Component)]
struct NextMoveText;
// Initialize UiCamera and text
fn init_next_move_text(mut commands: Commands, asset_server: ResMut<AssetServer>) {
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
commands.spawn(Camera2dBundle {
camera: Camera {
order: 2,
..default()
},
camera_2d: Camera2d {
clear_color: ClearColorConfig::None,
..default()
},
..default()
});
commands.spawn((
TextBundle::from_section(
"Next Move: White", TextStyle {
font: font,
font_size: 40.0,
color: Color::rgb(0.8, 0.8, 0.8)
}
)
.with_text_alignment(TextAlignment::Center)
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
left: Val::Px(10.),
top: Val::Px(10.),
..default()
},
..default()
}),
NextMoveText
));
}
fn next_move_text_update(mut _commands: Commands, turn: Res<PlayerTurn>, mut query: Query<(&mut Text, &NextMoveText)>) {
if !turn.is_changed() {
return;
}
for (mut text, _tag) in query.iter_mut() {
text.sections[0].value = format!("Next move: {}", match turn.0 {
PieceColor::White => "White",
PieceColor::Black => "Black"
});
}
}
pub struct UIPlugin;
impl Plugin for UIPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(init_next_move_text)
.add_system(next_move_text_update);
}
}
You also have to change PlayerTurn
to be public:
pub struct PlayerTurn(pub PieceColor);
You will have to copy the Font from here into the assets/fonts
folder, or replace it with any other font you have lying around. Remember to also add the plugin in main.rs
!
Update: Because of how the camera works in ui.rs
, you need to change the Camera3dBundle
in main.rs
to the following:
fn setup(mut commands: Commands) {
// Camera
commands.spawn((Camera3dBundle {
transform: Transform::from_matrix(Mat4::from_rotation_translation(
Quat::from_xyzw(-0.3, -0.5, -0.3, 0.5).normalize(),
Vec3::new(-7., 20., 4.)
)),
camera: Camera {
order: 1,
..default()
},
..default()
}, RaycastPickCamera::default()));
// Light
commands.spawn(PointLightBundle {
transform: Transform::from_translation(Vec3::new(4., 8., 4.)),
..default()
});
}
If you now run the game, you should see a small text at the top that shows the current turn:
![https://caballerocoll.com/images/bevy_chess_ui.png]
The init_next_move_text
system doesn't have anything special, it just creates an entity with Camera2dBundle
, and a TextBundle
which has the NextMoveText
component as a tag, to be able to tell it apart from other Text
entities (there are none in our case, but you might want to add more down the line).
next_move_text_update
is a bit more interesting. We don't need it to run every frame, because the turn will change only sparsely. We can use the is_changed
function for that purpose! Bevy will only run this system when PlayerTurn
has changed, this way we don't waste time updating the text when the turn has not changed.
There are similar query transformers we could use for querying components, but we haven't needed them. There's Added<T>
, which will only run for entities that have had T
added, Mutated<T>
, which will run for entities that have had their T
component change, and Changed<T>
, which is a combination of Mutated
and Added
.
The following is an example of a system that prints to the console changes to the Text component:
fn log_text_changes(query: Query<&Text, Mutated<Text>>) {
for text in query.iter() {
println!("New text: {}", text.value);
}
}
If you add this system to the UIPlugin
, you'll see the text printed to the console every time it changes. If the query was query: Query<&Text>
instead, it would run every frame, and you'd see your console filled with output.
We have most of the game logic on our OnPointer::<Click>
closure, which would be fine if we leave this as a prototype (or if you enjoy code archaeology with three month old functions where you don't remember anything and you also didn't leave comments because it's "self explanatory". I'm not speaking from experience 😌), but it doesn't really work if we want to be Responsible Programmers™. Let's clean it up!
This change is going to be big, so get ready!
First, we'll add a method to PlayerTurn
, to keep things a bit neater:
impl PlayerTurn {
fn change(&mut self) {
self.0 = match self.0 {
PieceColor::White => PieceColor::Black,
PieceColor::Black => PieceColor::White
}
}
}
Next, we'll replace our OnPointer::<Click>
closure for the following, which is much more representative of it's name, as it only deals with selecting a square. Update: During this entire tutorial, I've kept the OnPointer::<Click>
closure as a closure instead of making it a function (as with bevy_mod_picking
, that is something you can do). Here, I will move it out of the closure and into a proper function along with changing the code as in the original tutorial. It keeps the general part of selecting the square it previously had, but we removed all the extra moving, taking and despawning code:
fn select_square(In(event): In<ListenedEvent<Click>>, mut selected_square: ResMut<SelectedSquare>, mut selected_piece: ResMut<SelectedPiece>, squares_query: Query<&Square>) -> Bubble {
if let Ok(_) = squares_query.get(event.target) {
selected_square.entity = Some(event.target);
} else {
selected_square.entity = None;
selected_piece.entity = None;
}
Bubble::Up
}
Next, we'll add a select_piece
system, that takes care of selecting the piece that is in the selected square. You'll notice that we use is_changed
function from Res<SelectedSquare>
, so that it only runs when we select a new square. It also exits early if there is no square to be found, and only changes the selected piece if there is none selected currently:
fn select_piece(selected_square: Res<SelectedSquare>, mut selected_piece: ResMut<SelectedPiece>, turn: Res<PlayerTurn>, squares_query: Query<&Square>, pieces_query: Query<(Entity, &Piece)>) {
if !selected_square.is_changed() {
return;
}
let square_entity = if let Some(entity) = selected_square.entity {
entity
} else {
return;
};
let square = if let Ok(square) = squares_query.get(square_entity) {
square
} else {
return;
};
if selected_piece.entity.is_none() {
// Select the piece in the currently selected square
for (piece_entity, piece) in pieces_query.iter() {
if piece.x == square.x && piece.y == square.y && piece.color == turn.0 {
// piece_entity is now the entity in the same square
selected_piece.entity = Some(piece_entity);
break;
}
}
}
}
Most of the complexity for moving is still there to deal with the borrow checker, but it's now isolated in a single system: move_piece
. It also only runs when the selected square changes, with is_changed
from Res<SelectedPiece>
. Here we encounter a problem: we want to run this system only when the selected square has changed, but we also want to change it after we're done. Events to the rescue! We'll create an event that will take care of resetting SelectedSquare
, so we can call it from move_piece
:
fn move_piece(mut commands: Commands, selected_square: Res<SelectedSquare>, selected_piece: Res<SelectedPiece>, mut turn: ResMut<PlayerTurn>, squares_query: Query<&Square>, mut pieces_query: Query<(Entity, &mut Piece)>, mut reset_selected_event: EventWriter<ResetSelectedEvent>) {
if !selected_square.is_changed() {
return;
}
let square_entity = if let Some(entity) = selected_square.entity {
entity
} else {
return;
};
let square = if let Ok(square) = squares_query.get(square_entity) {
square
} else {
return;
};
if let Some(selected_piece_entity) = selected_piece.entity {
let pieces_vec = pieces_query
.iter_mut()
.map(|(_, piece)| *piece)
.collect::<Vec<Piece>>();
let pieces_entity_vec = pieces_query
.iter_mut()
.map(|(entity, piece)| (entity, *piece))
.collect::<Vec<(Entity, Piece)>>();
// Move the selected piece to the selected square
let mut piece = if let Ok((_piece_entity, piece)) = pieces_query.get_mut(selected_piece_entity) {
piece
} else {
return;
};
if piece.is_move_valid((square.x, square.y), pieces_vec) {
// Check if a piece of ther opposite color exists in this square and despawn it
for (other_entity, other_piece) in pieces_entity_vec {
if other_piece.x == square.x && other_piece.y == square.y && other_piece.color != piece.color {
// Mark the piece as taken
commands.entity(other_entity).insert(Taken);
}
}
// Move piece
piece.x = square.x;
piece.y = square.y;
// Change turn
turn.change();
}
reset_selected_event.send(ResetSelectedEvent);
}
}
struct ResetSelectedEvent;
fn reset_selected(mut event_reader: EventReader<ResetSelectedEvent>, mut selected_square: ResMut<SelectedSquare>, mut selected_piece: ResMut<SelectedPiece>) {
for _event in event_reader.iter() {
selected_square.entity = None;
selected_piece.entity = None;
}
}
We've created an empty struct called ResetSelectedEvent
, which we can send using the send function in the Event_Writer<ResetSelectedEvent>
struct. Then, the reset_selected
system will consume this events and set SelectedSquare
and SelectedPiece
to None
.
Instead of despawning the piece and it's children directly from the move_system
, we'll add a Taken
component to the piece using the insert
function from EntityCommands
(that we get by calling entity
from Commands
), which just adds a component to an entity.
We will then have another system that takes care of despawning pieces with the Taken
component, and checking if we should quit the game:
#[derive(Component)]
struct Taken;
fn despawn_taken_pieces(mut commands: Commands, mut app_exit_events: EventWriter<AppExit>, query: Query<(Entity, &Piece, &Taken)>) {
for (entity, piece, _taken) in query.iter() {
// If the king is taken, we should exit
if piece.piece_type == PieceType::King {
println!("{} won! Thanks for playing!", match piece.color {
PieceColor::White => "Black",
PieceColor::Black => "White"
});
app_exit_events.send(AppExit);
}
// Despawn piece and children
commands.entity(entity).despawn_recursive();
}
}
We could have used Added<Taken>
in this query, but as we directly remove the entities, there won't be any entity with the Taken
component that hasn't just had it added.
Finally, we need to add all of these systems and events to the board plugin:
pub struct BoardPlugin;
impl Plugin for BoardPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SelectedSquare>()
.init_resource::<HoverSquare>()
.init_resource::<SelectedPiece>()
.init_resource::<PlayerTurn>()
.add_event::<ResetSelectedEvent>()
.add_startup_system(create_board)
.add_system(color_squares)
.add_system(select_piece)
.add_system(move_piece.before(select_piece))
.add_system(reset_selected)
.add_system(despawn_taken_pieces);
}
}
And we're done! The code isn't the best thing ever written, but we can now see each system doing it's own different thing. You can now play our finished chess!
![https://caballerocoll.com/images/bevy_chess_all_done.gif]
The following section deals with updating things that have been fixed or changed in Bevy after the release of this tutorial. You don't really need to implement these, but I think it will provide a higher code quality. These aren't implemented already in the tutorial because it doesn't result in it not working.
One thing we'll change is how we change the square colors. Currently, due to a bug in Bevy, we had a different material for each square, and we changed the albedos in each of them to reflect the state. This isn't a huge problem when we have 64 materials, but it's cleaner to not have so much repeated data. We'll change this to instead have a Resource
that keeps 4 material handles to each of the colors we want to use, and we'll swap the handles instead of the base colors.
First things we'll do is create the resource, which we'll call SquareMaterials
, and we'll implement the FromResources
trait:
struct SquareMaterials {
highlight_color: Handle<StandardMaterial>,
selected_color: Handle<StandardMaterial>,
black_color: Handle<StandardMaterial>,
white_color: Handle<StandardMaterial>
}
impl FromWorld for SquareMaterials {
fn from_world(world: &mut World) -> Self {
let world = world.cell();
let mut materials = world.get_resource_mut::<Assets<StandardMaterial>>().unwrap();
SquareMaterials {
highlight_color: materials.add(Color::rgb(0.8, 0.3, 0.3).into()),
selected_color: materials.add(Color::rgb(0.9, 0.1, 0.1).into()),
black_color: materials.add(Color::rgb(0., 0.1, 0.1).into()),
white_color: materials.add(Color::rgb(1., 0.9, 0.9).into())
}
}
}
The FromWorld
trait allows us to initialize the resource properly, with each of the materials being added to Bevy's Assets<StandardMaterial>
resource. We also have to tell Bevy to initialize the Resource, so we'll have to add .init_resource::<SquareMaterials>()
to BoardPlugin
.
After that, we need to change the create_board
and color_squares
systems to use the resource;
fn create_board(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, materials: Res<SquareMaterials>) {
// Add meshes and materials
let mesh = meshes.add(Mesh::from(shape::Plane { size: 1., ..default() }));
// Spawn 64 squares
for i in 0..8 {
for j in 0..8 {
commands.spawn((PbrBundle {
mesh: mesh.clone(),
// Change material according to position to get alternating pattern
material: if (i + j + 1) % 2 == 0 {
materials.white_color.clone()
} else {
materials.black_color.clone()
},
transform: Transform::from_translation(Vec3::new(i as f32, 0., j as f32)),
..default()
}, PickableBundle::default(),
RaycastPickTarget::default(),
Square {
x: i,
y: j
},
OnPointer::<Click>::run_callback(select_square),
OnPointer::<Over>::run_callback(|In(event): In<ListenedEvent<Over>>, mut hover_square: ResMut<HoverSquare>| {
hover_square.entity = Some(event.target);
Bubble::Up
})));
}
}
}
fn color_squares(selected_square: Res<SelectedSquare>, hover_square: Res<HoverSquare>, materials: Res<SquareMaterials>, mut query: Query<(Entity, &Square, &mut Handle<StandardMaterial>)>) {
for (entity, square, mut material) in query.iter_mut() {
*material = if Some(entity) == hover_square.entity {
materials.highlight_color.clone()
} else if Some(entity) == selected_square.entity {
materials.selected_color.clone()
} else if square.is_white() {
materials.white_color.clone()
} else {
materials.black_color.clone()
};
}
}
And that's it! Pretty simple change, we just replaced the base color changing with a change to the actual handles.
This concludes our Chess game! We went over most of the concepts in Bevy, so you should have a good grasp and be able to work on your own games now. If you finished the tutorial, don't hesitate to post a video on Twitter and tag me so I can see it!
If you don't have any particular idea for a game, here's some stuff you could try as homework to improve the game and to practice a bit:
- Implement a system that keeps track of the taken pieces and displays them, maybe in UI or as objects next to the board.
- Finish implementing all the rules: castling, en passant, check mates, etc.
- Make up new rules! Make your own chess variation and start trying different things.
- Try moving the camera around!
- Make the movement animations finish before allowing the next move to be made and before the pieces are despawned. You can keep a resource that has a
piece_in_movement
boolean, and don't run the picking system while that is true. - Advanced: Make a resource that keeps track of all the moves in a vector, so they can be reverted and saved.
This tutorial's text is licensed under the MIT license.