To start off a 2048 game we need to spawn two tiles onto the board. To do that we're going to create a new startup system that will handle picking board positions and spawning the tiles.
We're going to create two new Components to help us with the playing tiles. Remember that Components hold data or are labels for entities.
Our first component, Points will hold the value of the tile. In 2048, that value is in the sequence 2^n: 2,4,8,16,32,etc because our tiles start at 2 and combine with tiles of the same point value.
#[derive(Component)]
struct Points {
value: u32,
}
Our second component holds the position of the tile on the grid with an x and a y value.
#[derive(Component)]
struct Position {
x: u8,
y: u8,
}
The spawn_tiles System
The spawn_tiles system is going to be responsible for spawning two tiles to start the game.
The system will need
Commandsto spawn tile spritesQuery<&Board>to get the board size
fn spawn_tiles(
mut commands: Commands,
query_board: Query<&Board>,
) {
let board = query_board.single();
let mut rng = rand::thread_rng();
let starting_tiles: Vec<(u8, u8)> = (0..board.size)
.cartesian_product(0..board.size)
.choose_multiple(&mut rng, 2);
for (x, y) in starting_tiles.iter() {
let pos = Position { x: *x, y: *y };
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: colors::TILE,
custom_size: Some(Vec2::new(
TILE_SIZE, TILE_SIZE,
)),
..default()
},
transform: Transform::from_xyz(
board.cell_position_to_physical(pos.x),
board.cell_position_to_physical(pos.y),
1.0,
),
..default()
})
.insert(Points { value: 2 })
.insert(pos);
}
}
The board query will only get one result ever because we only have one board, so we can use .single() on the query to get the single board. [single](https://docs.rs/bevy/0.10.0/bevy/ecs/system/struct.Query.html#method.single) will panic if the number of results in the query isn’t exactly one which is fine for us since if we don't have a board, we can't continue.
let board = query_board.single();
Then we'll use the rand crate to set up a random number generator.
Be sure to add the rand crate using cargo add
cargo add rand@0.8.5
After generating all of the possible grid locations, the random number generator will allow us to choose two of them, randomly.
starting_tiles will be our two tiles to spawn on the board. We store them in a Vec as a two-tuple of u8 numbers. We already talked about using .cartesian_product to generate all the grid tiles and we do the same here.
After generating the iterator of (u8,u8), we can chain .choose_multiple` to use the random number generator to pick two of them.
let mut rng = rand::thread_rng();
let starting_tiles: Vec<(u8, u8)> = (0..board.size)
.cartesian_product(0..board.size)
.choose_multiple(&mut rng, 2);
choose_multiple comes from an extension trait in the rand prelude, so we'll want to bring that trait into scope as well, alongside the rest of the prelude.
use rand::prelude::*;
with the starting_tiles picked out, we can iterate over the tuples and insert each of the titles. First we'll destructure the x and y values for each item using a for loop.
After destructuring the values we can create a new Position component with the x and y values. We do this early in the functions because it makes it a bit more clear when we use it later on.
Iterating over the starting_tiles gives us a reference into the tuple values, so we need to dereference them to construct the Position component with the x and y u8 values, not references to the u8 values.
for (x, y) in starting_tiles.iter() {
let pos = Position { x: *x, y: *y };
Spawning a SpriteBundle using commands is the same as spawning the board and the tile placeholders. The sprite will be TILE_SIZE width and height size. The color will be a new TILE color that we'll add to the colors module, and we fill in the rest of the fields with the default()s.
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: colors::TILE,
custom_size: Some(Vec2::new(
TILE_SIZE, TILE_SIZE,
)),
..default()
},
transform: Transform::from_xyz(
board.cell_position_to_physical(pos.x),
board.cell_position_to_physical(pos.y),
1.0,
),
..default()
})
Which leaves us with the Transform. The Transform will be the same calculation that we used to place the tile_placeholders, so we can use Board::cell_position_to_physical to find the right place for them.
Finally we insert the Points component with a default value of 2 and the Position component we created earlier.
.insert(Points { value: 2 })
.insert(pos);
After building out the system, we need to add a color to the colors module:
pub const TILE: Color = Color::Lcha {
lightness: 0.85,
chroma: 0.5,
hue: 315.0,
alpha: 1.0,
};
and then we need to run the spawn_tiles system at startup, alongside setup and spawn_board.
.add_startup_systems((
setup,
spawn_board,
spawn_tiles,
))
but the game immediately panics and crashes.
thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: NoEntities("bevy_ecs::query::state::QueryState<&boxes::Board>")', src/main.rs:120:29
This is because spawn_tiles depends on the Board being inserted on an entity, so we need to add some ordering to our startup systems.
Adding order to a set of systems can be done a couple of ways. We’ll be using .chain to set an order for all of the systems in a tuple. .chain will execute all of our systems in order.
We also need apply_system_buffers after spawn_board.
.add_startup_systems(
(
setup,
spawn_board,
apply_system_buffers,
spawn_tiles,
)
.chain(),
)
We need apply_system_buffers because of the way commands work in Bevy. Commands like spawn are queued up in each system, and executed later in a batch. If we need access to something we added via a command in a previous system, we have to make sure that the commands are applied. apply_system_buffers is a system from Bevy’s prelude that does just that: it applies the queued up commands.
After the commands are applied, spawn_tiles can find the Board component on the entity we spawned.
Our full main function now looks like this
fn main() {
App::new()
.insert_resource(ClearColor(
Color::hex("#1f2638").unwrap(),
))
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "2048".to_string(),
..default()
}),
..default()
}))
.add_startup_systems(
(
setup,
spawn_board,
apply_system_buffers,
spawn_tiles,
)
.chain(),
)
.run()
}
and every time we re-run our app, we get new tiles on the board.