Spawning a new snake segment in tick
is something we could copy/paste the code for from the original snake spawn in spawn_board
.
Instead of that, we’re going to make our own custom Bevy command to make it a bit more clear what’s happening.
The Commands
we use are how we mutate the World
, and the trait each of these commands implements is called Command
. Our goal is to implement Command
to create our own spawn snake command.
In board.rs
create a new public struct called SpawnSnakeSegment
which will be the struct we use to drive our command. Think of this like the arguments to a function.
pub struct SpawnSnakeSegment {
pub position: Position,
}
Bring command into scope:
use bevy::{ecs::system::Command, prelude::*};
and we can start implementing Command
for SpawnSnakeSegment
. The core of the Command
trait is the write
function, which gives us self
, which is our SpawnSnakeSegment
and a mutable world to modify.
impl Command for SpawnSnakeSegment {
fn write(self, world: &mut World) {
...
}
}
We are freely available to use this world reference to grab anything we want. We can use it to grab a reference to the board
by query
ing the world in the same way our systems do. The biggest difference here is that world
is a bit lower level than our systems, so the functions that are available to us change a bit.
For example, we can query for &Board
but we don’t have .single()
available to us anymore, so we use .iter
, which also accepts an argument. We know there’s only one board, so we can call .next()
on the iterator to get an Option<&Board>
and .unwrap()
to get the &Board
.
Also note that we don’t have access to spawn_bundle
anymore. We need to first .spawn
an entity and then we can insert_bundle
on that entity.
Finally also note that we’re using self
to access the position.
impl Command for SpawnSnakeSegment {
fn write(self, world: &mut World) {
let board = world
.query::<&Board>()
.iter(&world)
.next()
.unwrap();
world
.spawn()
.insert_bundle(SpriteBundle {
sprite: Sprite {
color: COLORS.snake,
custom_size: Some(Vec2::new(
TILE_SIZE, TILE_SIZE,
)),
..Sprite::default()
},
transform: Transform::from_xyz(
board.cell_position_to_physical(
self.position.x,
),
board.cell_position_to_physical(
self.position.y,
),
2.0,
),
..Default::default()
})
.insert(self.position);
}
}
The core spawning logic here is a copy/paste from the spawn_board logic where we were spawning the snake segments originally.
The code here does have a flaw though and it’s an important one to point out.
error[E0502]: cannot borrow `*world` as mutable because it is also borrowed as immutable
--> src/board.rs:119:9
|
115 | .iter(&world)
| ------ immutable borrow occurs here
...
119 | / world
120 | | .spawn()
| |____________________^ mutable borrow occurs here
...
130 | / board.cell_position_to_physical(
131 | | self.position.x,
132 | | ),
| |_____________________- immutable borrow later used here
We get a board from the world, which means we’re holding a small piece of the world as a shared reference in the board
variable.
When we go to use world.spawn()
we are trying to mutate the world, which requires an exclusive (also known as mutable) reference. This is ok so far because the board hasn’t been used yet.
Unfortunately for us, we use the board after the world.spawn()
to determine the cell positions. This means we’re trying to hold onto the shared reference to the board (which is a piece of world
while also using a mutable reference to world
.
We can not hold both a shared reference and an exclusive reference at the same time.
Luckily for us, we don’t actually need to use the board that late in the program, we can move our .cell_position_to_physical
calls up above the world.spawn()
call which means that we acquire and stop using the board
before world.spawn
happens, allowing us to drop the shared reference and take the exclusive reference.
impl Command for SpawnSnakeSegment {
fn write(self, world: &mut World) {
let board = world
.query::<&Board>()
.iter(&world)
.next()
.unwrap();
let x = board
.cell_position_to_physical(self.position.x);
let y = board
.cell_position_to_physical(self.position.y);
world
.spawn()
.insert_bundle(SpriteBundle {
sprite: Sprite {
color: COLORS.snake,
custom_size: Some(Vec2::new(
TILE_SIZE, TILE_SIZE,
)),
..Sprite::default()
},
transform: Transform::from_xyz(x, y, 2.0),
..Default::default()
})
.insert(self.position);
}
}
Back up in spawn_board
we can now use our custom command by calling commands.add
with out SpawnSnakeSegment
that implements Command
. The segment can be dereferenced, which will use the Copy
implementation on Position
, as I’m doing here or cloned with .clone
if that is how you want to write it. It amounts to the same thing.
for segment in snake.segments.iter() {
commands.add({
SpawnSnakeSegment { position: *segment }
});
}
Don’t forget to remove the extra Board
we built in spawn_board
.
let board = Board::new(20);
Back in lib.rs
, right after push_front
, add the same SpawnSnakeSegment
command using next_position
to insert the new snake head.
snake.segments.push_front(next_position);
commands.add({
SpawnSnakeSegment {
position: next_position,
}
});
After running cargo run
this will result in the snake running away off the right side of the screen.