One apple is nice, but to play a full game of snake we need to spawn an apple every time one gets eaten.
We’re going to need the rand
crate to randomly pick a place to spawn apples, so add that now.
cargo add rand
Our apple spawning system is going to be driven by events. When the snake eats an apple, we’ll fire off a NewFoodEvent
and there will be a system listening to those events that will handle spawning a new apple on the board.
Update the code in food.rs
to have a new FoodPlugin
, a NewFoodEvent
, and our food_event_listener
.
use bevy::prelude::*;
use itertools::Itertools;
use rand::prelude::SliceRandom;
use crate::{
board::{Board, Position, SpawnApple},
snake::Snake,
};
pub struct FoodPlugin;
impl Plugin for FoodPlugin {
fn build(&self, app: &mut App) {
app.add_event::<NewFoodEvent>()
.add_system(food_event_listener);
}
}
pub struct NewFoodEvent;
#[derive(Component)]
pub struct Food;
pub fn food_event_listener(
mut commands: Commands,
query_board: Query<&Board>,
mut events: EventReader<NewFoodEvent>,
snake: Res<Snake>,
) {
let board = query_board.single();
let possible_food_locations = (0..board.size)
.cartesian_product(0..board.size)
.map(|point| Position {
x: point.0,
y: point.1,
})
.filter(|pos| !snake.segments.contains(pos))
.collect::<Vec<Position>>();
let mut num_food = 0;
for _ in events.iter() {
num_food += 1;
}
let mut rng = rand::thread_rng();
for pos in possible_food_locations
.choose_multiple(&mut rng, num_food)
{
commands.add(SpawnApple { position: *pos });
}
}
We have to register our new event with Bevy, which our FoodPlugin
does for us, as well as adding a constantly running system that runs our event listener.
Bevy Events are regular structs, so we could have a food event that had data fields but in this case we’ll use a “unit struct” that has no fields. It basically acts as a marker that says “hey an event of this type happened” and nothing else.
Our food_event_listener
is going to build up a full list of all the board tiles, then filter that list to remove any positions that the snake is already on, and finally pick one of the remaining tiles randomly to spawn an apple on.
The EventReader
argument in the listener system is the only new type in the system arguments that we haven’t seen yet. It gives us access to an iterator whose items are NewFoodEvent
events we haven’t seen yet.
When we iterate over the EventReader
it marks events as “seen”, which is why we need it to be mutable. It’s not a Vec
of events and doesn’t act like one. We have to use the iterator functionality to pull events off.
possible_food_locations
uses the cartesian_product from itertools like we’ve seen before to generate a full list of all board tiles. We map over this list and turn all of the tuples into Position
structs so that we can compare them to the snake segments and filter out any Position
s that the snake is on.
Because we have to use the iterator functionality in the EventReader
, but we really only need to know how many tiles to choose, we do a small loop and count the events ourselves.
EventReader
does have a len
function on it, but that won’t consume any of the events so next time we ran the system the events would still exist. We need to consume the events so they aren’t processed multiple times.
rand
is a random number generator utility crate. It includes helpful traits and functions for dealing with randomness.
In our case, we start up a new random number generator and then take advantage of the SliceRandom
trait from the rand
crate to pick a number of the tiles.
We can see from it’s name that the SliceRandom
trait allows us to treat our Vec<Position>
of possible_food_locations
as a slice, from which it picks two different tiles.
We can immediately iterate over the choices rand chose for us, and spawn an apple in those positions.
Technically speaking, while it is possible we could spawn multiple apples here, we shouldn’t see that happen unless you want to play around with sending multiple events (try it, it’s fun!).
Don’t forget to bring the relevant items we used into scope!
Since we access Board
here, the board in board.rs
will have to be made public. It’s field size
will as well.
pub struct Board {
pub size: u8,
physical_size: f32,
}
In main.rs
add the FoodPlugin
with the rest of the plugins.
.add_plugins(DefaultPlugins)
.add_plugin(ControlsPlugin)
.add_plugin(FoodPlugin)
Sending Events
With all of the listener logic and events set up, we can head over to tick
in lib.rs
and update the function signature to accept an EventWriter
.
This will let us send NewFoodEvent
s when an apple is eaten.
pub fn tick(
mut commands: Commands,
mut snake: ResMut<Snake>,
positions: Query<(Entity, &Position)>,
input: Res<controls::Direction>,
query_food: Query<(Entity, &Position), With<Food>>,
mut food_events: EventWriter<NewFoodEvent>,
) {
Later on in our is_food
match, we can use the EventWriter
to send an event after we despawn an apple.
Some((entity, _)) => {
commands.entity(entity).despawn_recursive();
food_events.send(NewFoodEvent);
}
and that’s how you get a long snake!