Log in to access Rust Adventure videos!

Lesson Details

We're going to place down a series of tile positions inside of the board to form our grid to play on. To do that we'll add a new Color to the colors module called TILE_PLACEHOLDER.

pub const TILE_PLACEHOLDER: Color = Color::Lcha {
    lightness: 0.55,
    chroma: 0.5,
    hue: 315.0,
    alpha: 1.0,
};

Back in src/main.rs, .with_children will give us a builder that we can use the same way we used commands, which means we can use spawn to render sprites as children of the board.

We do mostly the same thing as before, but with colors::TILE_PLACEHOLDER color instead of the colors::BOARD color. 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(SpriteBundle {
        sprite: Sprite {
            color: colors::TILE_PLACEHOLDER,
            custom_size: Some(Vec2::new(
                TILE_SIZE, TILE_SIZE,
            )),
            ..default()
        },
        transform: Transform::from_xyz(
            0.0, 0.0, 1.0,
        ),
        ..default()
    });
})
.insert(board);
Sprite placement

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.

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 the tile 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.

let offset = -physical_board_size / 2.0;
bottom left sprite

Then 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;

Then we use the full offset value to change the x and y position of the tile, placing it squarely in the bottom left board cell.

transform: Transform::from_xyz(
    offset, offset, 1.0,
),
bottom left sprite

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 Ranges and Iterators.

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:51] tile = 0
[src/main.rs:51] tile = 1
[src/main.rs:51] tile = 2
[src/main.rs:51] 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@0.10.5

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 by bringing the extension trait Itertools 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. “cartesian product” is a mathy word that means if the first range is all of the x values in our grid, and the second range is all of the y values in our grid, then what we get is an iterator over all of the x,y positions for all of the cells in that grid.

for tile in (0..board.size)
    .cartesian_product(0..board.size)
{
    dbg!(tile);
}

The values from each Range are combined into a tuple, which looks like this when logged out.

[src/main.rs:54] tile = (0,0)
[src/main.rs:54] tile = (0,1)
[src/main.rs:54] tile = (0,2)
[src/main.rs:54] tile = (0,3)
[src/main.rs:54] tile = (1,0)
[src/main.rs:54] tile = (1,1)
[src/main.rs:54] tile = (1,2)
[src/main.rs:54] tile = (1,3)
[src/main.rs:54] tile = (2,0)
[src/main.rs:54] tile = (2,1)
[src/main.rs:54] tile = (2,2)
[src/main.rs:54] tile = (2,3)
[src/main.rs:54] tile = (3,0)
[src/main.rs:54] tile = (3,1)
[src/main.rs:54] tile = (3,2)
[src/main.rs:54] tile = (3,3)

Tuples can be collections of any sort of value. In our case we have tuples of 2 u8s, 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.

.with_children(|builder| {
    let offset = -physical_board_size / 2.0
        + 0.5 * TILE_SIZE;

    for tile in (0..board.size)
        .cartesian_product(0..board.size)
    {
        builder.spawn(SpriteBundle {
            sprite: Sprite {
                color: colors::TILE_PLACEHOLDER,
                custom_size: Some(Vec2::new(
                    TILE_SIZE, TILE_SIZE,
                )),
                ..default()
            },
            transform: Transform::from_xyz(
                offset
                    + f32::from(tile.0) * TILE_SIZE,
                offset
                    + f32::from(tile.1) * TILE_SIZE,
                1.0,
            ),
            ..default()
        });
    }
})

This gives us what looks almost exactly like the board we already had... because we didn't make any gaps between the tiles.

full board

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. A good place for it is at the top of the file next to TILE_SIZE.

const TILE_SPACER: f32 = 10.0;

Then in our physical_board_size variable at the top of spawn_board we need to add a space before every tile, plus one at the end so that it looks uniform. Our board also needs to accommodate that extra space.

let physical_board_size = f32::from(board.size)
    * TILE_SIZE
    + f32::from(board.size + 1) * TILE_SPACER;
board with space

and in our offset for each tile, we add N spaces. If it's the first tile (at index 0), 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.

end result