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 Positions 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 NewFoodEvents 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!