We're going to place down a series of tile positions inside of the board. To do that we'll need a new ColorMaterial
to Materials
.
struct Materials {
board: Handle<ColorMaterial>,
tile_placeholder: Handle<ColorMaterial>,
}
And we'll add a new Color
in the FromWorld
implementation.
impl FromWorld for Materials {
fn from_world(world: &mut World) -> Self {
let mut materials = world
.get_resource_mut::<Assets<ColorMaterial>>()
.unwrap();
Materials {
board: materials
.add(Color::rgb(0.7, 0.7, 0.8).into()),
tile_placeholder: materials
.add(Color::rgb(0.75, 0.75, 0.9).into()),
}
}
}
.with_children
will give us a builder that we can use the same way we used commands
, which means we can use spawn_bundle
to render sprites as children of the board.
We do mostly the same thing as before, but with tile_placeholder
material instead of the board
material. Most importantly we specify a transform
field so that we can set the z-index. This will layer our new tile placeholders over the top of the board material.
.with_children(|builder| {
builder.spawn_bundle(SpriteBundle {
material: materials
.tile_placeholder
.clone(),
sprite: Sprite::new(Vec2::new(
TILE_SIZE, TILE_SIZE,
)),
transform: Transform::from_xyz(
0.0, 0.0, 1.0,
),
..Default::default()
});
})
.insert(board);
This brings up an important aspect of how Bevy handles Sprites. The center of the sprite is where x:0, y:0 is. If you place a sprite at x:1, y:2, then the center of the sprite will be at x:1, y:2.
So we're going to re-center the sprite to make x:0, y:0 be the bottom-left box in the board grid. To do this we'll take half the board size and move it from the center to the left and downward using a negative value. This places the center of the tile on the bottom left corner of the board.
So we need to move the tile half a tile size up and to the right which places the tile in the bottom left grid slot.
let offset = -physical_board_size / 2.0
+ 0.5 * TILE_SIZE;
transform: Transform::from_xyz(
offset, offset, 1.0,
),
Rendering more tiles
So now we can render one tile in a grid position that we want it, but we need a tile placeholder for every spot. To do this we'll take advantage of Range
s and Iterator
s.
0..board.size
is called a Range
and will give us the numbers from 0 to the size of the board. We can use a for loop to iterate over each of the numbers.
This loop uses the dbg!
macro, which will print the source location, line number, and value of the expression we give it.
for tile in (0..board.size) {
dbg!(tile);
}
Which results in this output.
[src/main.rs:64] tile = 0
[src/main.rs:64] tile = 1
[src/main.rs:64] tile = 2
[src/main.rs:64] tile = 3
We don't just need one number though, we need numbers for every tile in the grid. To do that we can grab a function from the itertools
crate.
cargo add itertools
Itertools provides an extension trait for iterators that gives us additional functions on Ranges. Specifically we're going to bring the extra functions into scope.
use itertools::Itertools;
and then we'll use the cartesian_product
function to combine two ranges in a way that will give us each grid point.
for tile in (0..board.size)
.cartesian_product(0..board.size)
{
dbg!(tile);
}
The values are combined into a tuple, which looks like this when logged out.
[src/main.rs:67] tile = (0,0)
[src/main.rs:67] tile = (0,1)
[src/main.rs:67] tile = (0,2)
[src/main.rs:67] tile = (0,3)
[src/main.rs:67] tile = (1,0)
[src/main.rs:67] tile = (1,1)
[src/main.rs:67] tile = (1,2)
[src/main.rs:67] tile = (1,3)
[src/main.rs:67] tile = (2,0)
[src/main.rs:67] tile = (2,1)
[src/main.rs:67] tile = (2,2)
[src/main.rs:67] tile = (2,3)
[src/main.rs:67] tile = (3,0)
[src/main.rs:67] tile = (3,1)
[src/main.rs:67] tile = (3,2)
[src/main.rs:67] tile = (3,3)
Tuples can be collections of any sort of value. In our case we have tuples of 2 items, an x and a y position. The x position is the first item in the tuple, which is 0-indexed, so we access it by writing tile.0
.
We can shift the tile into it's position by taking which tile it is and multiplying that by the physical tile size, then adding it to the offset.
for tile in (0..board.size)
.cartesian_product(0..board.size)
{
builder.spawn_bundle(SpriteBundle {
material: materials
.tile_placeholder
.clone(),
sprite: Sprite::new(Vec2::new(
TILE_SIZE, TILE_SIZE,
)),
transform: Transform::from_xyz(
offset
+ f32::from(tile.0) * TILE_SIZE,
offset
+ f32::from(tile.1) * TILE_SIZE,
1.0,
),
..Default::default()
});
}
This gives us what looks almost exactly like the board we already had... because we didn't make any gaps between the tiles.
We'll add a new const named TILE_SPACER
to add in space between the tiles. Think of this as roughly "10 pixels" of physical space.
const TILE_SPACER: f32 = 10.0;
Then in our physical_board_size
we need to add a space before every tile, plus one at the end so that it looks uniform. So our board needs to accommodate that extra space.
let physical_board_size = f32::from(board.size)
* TILE_SIZE
+ f32::from(board.size + 1) * TILE_SPACER;
Then in our offset for each tile, we add N spaces. If it's the first tile, it gets one space, second gets two, and so on.
offset
+ f32::from(tile.0) * TILE_SIZE
+ f32::from(tile.0 + 1)
* TILE_SPACER,
Resulting in a spaced grid of tiles.