We’re going to be a bit more forward with our code organization on this project.
The default Rust package file locations for the binary entrypoint and the library entrypoint are src/main.rs
and src/lib.rs
respectively. We can and often do have both in the same package.
This lets us build up our game as a series of systems, plugins, and other items in lib.rs
and put them together like Jenga pieces in main.rs
. It also frees us up to write integration tests against the modules we expose from our library.
Create src/lib.rs
and create two public submodules by writing the following two lines in it.
pub mod board;
pub mod colors;
The default locations for these modules, after we’ve defined the sub-modules in lib.rs
, are src/board.rs
and src/colors.rs
, although we could technically put them anywhere. Remember: module paths and file paths are different things and don’t have to match!
Defining Colors
colors.rs
is going to be a utility file that we use to keep all of the colors we use for the board and such in the same place.
We’ll create a struct named Colors
to hold our colors. Each color has a name, in this case I’ve named them what they’re being used for.
The struct itself and all of the fields inside the struct need to be labelled pub
, because items are private by default in Rust’s module system. Using pub
will allow us to access these colors from outside of this file.
use bevy::prelude::Color;
pub struct Colors {
pub board: Color,
pub tile_placeholder: Color,
pub tile_placeholder_dark: Color,
}
pub const COLORS: Colors = Colors {
board: Color::rgb(0.42, 0.63, 0.07),
tile_placeholder: Color::rgb(0.62, 0.83, 0.27),
tile_placeholder_dark: Color::rgb(0.57, 0.78, 0.22),
};
We also declare a public constant called COLORS
. This is the value that holds an instantiated Colors
struct, and where we’ll define the colors we’re going to use.
I’ve chosen various shades of green for the board and the tile positions and you can choose whatever colors you want. The Color
enum from bevy contains a wide number of ways to define colors, so feel free to check out the documentation and pick one you like.
Spawning a Board
The board logic is a copy of the technique we used in the 2048 workshop.
In board.rs
, bring the bevy prelude and the Itertools
trait into scope. We’ll use itertools for building up an iterator of all of the possible tile positions on the board later.
use bevy::prelude::*;
use itertools::Itertools;
use crate::colors::COLORS;
const TILE_SIZE: f32 = 30.0;
const TILE_SPACER: f32 = 0.0;
#[derive(Component)]
struct Board {
size: u8,
physical_size: f32,
}
impl Board {
fn new(size: u8) -> Self {
let physical_size = f32::from(size) * TILE_SIZE
+ f32::from(size + 1) * TILE_SPACER;
Board {
size,
physical_size,
}
}
fn cell_position_to_physical(&self, pos: u8) -> f32 {
let offset =
-self.physical_size / 2.0 + 0.5 * TILE_SIZE;
offset
+ f32::from(pos) * TILE_SIZE
+ f32::from(pos + 1) * TILE_SPACER
}
}
pub fn spawn_board(mut commands: Commands) {
let board = Board::new(20);
commands
.spawn_bundle(SpriteBundle {
sprite: Sprite {
color: COLORS.board,
custom_size: Some(Vec2::new(
board.physical_size,
board.physical_size,
)),
..Sprite::default()
},
..Default::default()
})
.insert(board);
}
We can also bring our COLORS
into scope, using the crate::colors::COLORS
module path. The crate
module name is special, it indicates that we’re starting at the root of the crate. In this case, that means starting at lib.rs
. From there we can dive into the colors
sub-module defined in lib.rs
and finally specify the COLORS
const, which we declared public for this purpose.
The TILE_SIZE
and TILE_SPACE
constants will determine how big our tiles render and also how much space there is between them.
Next we define our Board
struct. We’ll be inserting this struct as a component on the entity where we render the board sprites, so we need to derive Component
.
The size
of the board is a u8
, which is an integer with a max value of 255. This is the number we’ll use when we talk about tile positions, like (3,4).
physical_size
is an internal value on the Board
struct although since we’ve defined the Board
struct in the same file we’re using it, we’ll also be able to access any of the fields inside of it. physical_size
represents how many pixels we should use to render the board on screen.
We implement two functions on the Board
type: new
and cell_position_to_physical
.
The new
function takes a grid size and constructs a new Board
, while also doing the calculation for how big we should render the board grid on screen.
The cell_position_to_physical
function takes an integer grid position and converts it to a position on screen using the TILE_SIZE
and TILE_SPACER
constants.
Finally we define a new system called spawn_board
. whose responsibility it is to create a new Board
, spawn an entity for it, and attach a Sprite
component so that it renders on screen. We also attach the Board
component to the same entity for convenience.
Back over in main.rs
, we can bring the spawn_board
system into scope and set it up as a startup system in our application.
use snake::board::spawn_board;
use bevy::prelude::*;
fn main() {
App::new()
.insert_resource(WindowDescriptor {
title: "Snake!".to_string(),
..Default::default()
})
.add_plugins(DefaultPlugins)
.insert_resource(ClearColor(Color::rgb(
0.52, 0.73, 0.17,
)))
.add_startup_system(setup)
.add_startup_system(spawn_board)
.run();
}
fn setup(mut commands: Commands) {
commands
.spawn_bundle(OrthographicCameraBundle::new_2d());
}
Notice that the module path we use now is snake::board::spawn_board
instead of starting with crate
. This is because we’ve crossed the crate boundary! We’re in the binary crate now, which is bringing in the spawn_board
system from the library crate.
If you used the same directory name as I did when we set up the project, then snake
is the name in Cargo.toml
. Our library, by default, will use this name as the name of our library.
After a cargo run
we should see a green box that represents the area our board takes up on screen.
Spawning the board tiles
To spawn the individual tiles on our board we can use the .with_children
function on the value returned from our spawn_bundle
where we spawned our Sprite
.
cartesian_product
is a function from the Itertools
trait and it’s a fancy name for taking all of the indexes on the x axis of our board grid and combining them with all of the indexes on the y axis of our board grid. It will give us a list of all of the cell positions in the board grid, as an iterator. That is: (0,0), (0,1), (0,2), etc
So we iterate over every tile position and spawn a Sprite
at each location.
commands
.spawn_bundle(SpriteBundle {
sprite: Sprite {
color: COLORS.board,
custom_size: Some(Vec2::new(
board.physical_size,
board.physical_size,
)),
..Sprite::default()
},
..Default::default()
})
.with_children(|builder| {
for (x, y) in (0..board.size)
.cartesian_product(0..board.size)
{
builder.spawn_bundle(SpriteBundle {
sprite: Sprite {
color: if (x + y) % 2 == 0 {
COLORS.tile_placeholder
} else {
COLORS.tile_placeholder_dark
},
custom_size: Some(Vec2::new(
TILE_SIZE, TILE_SIZE,
)),
..Sprite::default()
},
transform: Transform::from_xyz(
board.cell_position_to_physical(x),
board.cell_position_to_physical(y),
1.0,
),
..Default::default()
});
}
})
I’ve chosen to alternate tile colors for each Sprite
we render. We add the x position and y position together and then check to see if it’s divisible by 2 to determine which color to use.
A cargo run
now shows the full tile grid.
You could choose to add space between the tiles and you’d see the board color behind the tile placements, but I’ll choose to have a tile space of 0.0.
const TILE_SPACER: f32 = 1.0;