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
Commands
to 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_placeholder
s, 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.