Bevy has seen rapid adoption recently, but the learning resources are still fairly scarce. This is an attempt to provide a next step after the Bevy book. The final product looks like this:

It’s about 300 lines of Rust, so buckle in; this is a lengthy walkthrough. If you want to fast-forward to the finished code, the code lives here. For each section there’s a diff at the top, which should make it easier to determine where to place snippets of code, when it’s not clear.

An empty bevy app

See the diff for this section

We’ll start how the bevy book starts, with an app that does nothing. Run cargo new bevy-snake, then put the following in your main.rs:

use bevy::prelude::*;

fn main() {
    App::build().run();
}

We’ll need to add bevy as a dependency in our Cargo.toml, and because I have the ability to predict the future of this tutorial, let’s go ahead and add rand in there too so we have it when the time comes.

// ...

[dependencies]
bevy = "0.3.0"
rand = "0.7.3"

Creating a window

See the diff for this section

We’re going to be creating a 2D game, which requires a lot of different systems; one to create a window, one to do a render loop, one to handle input, one to handle sprites, etc. Luckily Bevy’s default plugins give us all of that:

fn main() {
    App::build().add_plugins(DefaultPlugins).run();
}

Bevy’s default plugins don’t include a camera though, so let’s insert a 2d camera, which we’ll set up by creating our first system:

fn setup(mut commands: Commands) {
    commands.spawn(Camera2dComponents::default());
}

Commands is used to queue up commands to mutate the world and resources. In this case, we’re spawning a new entity, with 2D camera components. Prepare for some bevy magic:

App::build()
    .add_startup_system(setup.system()) // <--
    .add_plugins(DefaultPlugins)
    .run();

All we have to do is call .system() on our function, and Bevy will auto-magically call it at startup with the commands param. Run the app again, and you should get an empty window like this:

The beginnings of a snake

See the diff for this section

Let’s try to get a snake head in there. We’ll define a couple structs:

struct SnakeHead;
struct Materials {
    head_material: Handle<ColorMaterial>,
}

SnakeHead is just an empty struct that we’ll use as a component, it’s sort of like a tag, we’ll put on an entity, and then we can find that entity later by querying for entities with the SnakeHead component. Empty structs like these are a common pattern in Bevy, components often don’t need any state of their own. Materials is going to be a resource, which stores the material that we’ll use for the snake head for now, and eventually for the snake segments and the food.

That head_material handle should be created when the game gets set up, so let’s do that next, by modifying our setup function:

fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
    commands.spawn(Camera2dComponents::default());
    commands.insert_resource(Materials {
        head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
    });
}
Bevy requires a specific ordering to the params when registering systems. Commands → Resources → Components/Queries. If you get a mysterious compile-time error after messing with a system, check your order.

materials.add will return a Handle<ColorMaterial>. We create a Materials struct using this new handle. Later, when we try to access a resource with the type of Materials, Bevy will find this struct we’ve created. We’ll now create our snake head entity in a new system, and you can see what using that resource looks like:

fn spawn_snake(mut commands: Commands, materials: Res<Materials>) {
    commands
        .spawn(SpriteComponents {
            material: materials.head_material.clone(),
            sprite: Sprite::new(Vec2::new(10.0, 10.0)),
            ..Default::default()
        })
        .with(SnakeHead);
}

Here we’ve got a new system, which will look for a resource of type Materials. It will then spawn a new entity, with SpriteComponents and SnakeHead as components. To create the SpriteComponents, we pass in the handle to the color material we created earlier, and give the sprite a size of (10,10). Let’s just add that system into our app builder:

.add_startup_system(setup.system())
.add_startup_stage("game_setup") // <--
.add_startup_system_to_stage("game_setup", spawn_snake.system()) // <--

The reason we need a new stage instead of just calling add_startup_system again, is that we need to use the material that gets inserted in the setup function. After running this, you should see a snake head in the middle of the screen:

Okay maybe calling it a snake head is a stretch, you’ll see a 10x10 white sprite.

Moving the snake

See the diff for this section

Snake isn’t much of a game without movement, so let’s get that head moving. We’ll worry about input later, for now our goal is just to get the head to move. So let’s create a system that will move all snake heads up:

fn snake_movement(mut head_positions: Query<(&SnakeHead, &mut Transform)>) {
    for (_head, mut transform) in head_positions.iter_mut() {
        *transform.translation.y_mut() += 2.;
    }
}

The main new concept here is that Query type. We can use it to iterate through all entities that have both the SnakeHead component and the Transform component. We don’t have to worry about actually creating that query, bevy will take care of creating it and calling our function with it, part of the ECS magic. So let’s add that system in and see what happens:

.add_startup_system_to_stage("game_setup", spawn_snake.system())
.add_system(snake_movement.system()) // <--
.add_plugins(DefaultPlugins)

Here’s what we get, a snake head moving off-screen:

You may be wondering about that Transform component. When we spawned the SnakeHead, we didn’t give it a Transform, so how come we’re able to find an entity that has a SnakeHead and a Transform component? What’s going on there is that SpriteComponents is a Bundle of components. For SpriteComponents, that means we get a Transform component, among a bunch of others (Sprite, Mesh, Draw, Rotation, Scale, etc).

Controlling the snake

See the diff for this section

Let’s modify our snake movement system to actually allow us to direct the snake:

fn snake_movement(
    keyboard_input: Res<Input<KeyCode>>,
    mut head_positions: Query<With<SnakeHead, &mut Transform>>,
) {
    for mut transform in head_positions.iter_mut() {
        if keyboard_input.pressed(KeyCode::Left) {
            *transform.translation.x_mut() -= 2.;
        }
        if keyboard_input.pressed(KeyCode::Right) {
            *transform.translation.x_mut() += 2.;
        }
        if keyboard_input.pressed(KeyCode::Down) {
            *transform.translation.y_mut() -= 2.;
        }
        if keyboard_input.pressed(KeyCode::Up) {
            *transform.translation.y_mut() += 2.;
        }
    }
}

Worth noting here is that we’ve changed the query from Query<&SnakeHead, &mut Transform>, to Query<With<SnakeHead, &mut Transform>>. There’s no reason it had to change now, the old query would have worked fine. I wanted the type signature to be simpler for the first system example, but now we can write it the correct way. The reason this is more correct is because we don’t need the SnakeHead component. So the With type allows us to say “I want entities that have a snake head, but I don’t care about the snake head component itself, just give me the transform”. The less components each system needs access to, the more bevy can parallelize the systems. For example, if another system was mutating the SnakeHead component, this system couldn’t be run in parallel if we had the old query.

Now we can control the snake, albeit in very un-snake-like fashion:

Slapping a grid on it

See the diff for this section

So far we’ve been using window coordinates, and the way that works is that (0,0) is the middle, and the units are pixels. Snake games generally use grids, so if we made our snake game 10x10, our window would be really small. Let’s make our lives a bit easier by using our own positioning and sizing. Then we can use systems that deal with transforming these to window coordinates.

We’ll make our grid 10x10. We’ll define those as constants up at the top of the file:

const ARENA_WIDTH: u32 = 10;
const ARENA_HEIGHT: u32 = 10;

Here are our new structs for positioning/sizing:

#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
struct Position {
    x: i32,
    y: i32,
}

struct Size {
    width: f32,
    height: f32,
}
impl Size {
    pub fn square(x: f32) -> Self {
        Self {
            width: x,
            height: x,
        }
    }
}

Fairly straightforward, with a helper method to get a Size with equal width and height. Position derives some traits that will be useful later, so we don’t have to keep going back to it. Size could really just have one float, since all objects will end up having equal width and height, but it feels wrong so I’m giving it a width and a height. Let’s add those components to the snake head we spawn:

commands
    .spawn(SpriteComponents {
        material: materials.head_material.clone(),
        sprite: Sprite::new(Vec2::new(10.0, 10.0)),
        ..Default::default()
    })
    .with(SnakeHead)
    .with(Position { x: 3, y: 3 }) // <--
    .with(Size::square(0.8)); // <--

These components aren’t doing anything currently, let’s start with transforming our sizes into sprite sizes:

fn size_scaling(windows: Res<Windows>, mut q: Query<(&Size, &mut Sprite)>) {
    let window = windows.get_primary().unwrap();
    for (sprite_size, mut sprite) in q.iter_mut() {
        sprite.size = Vec2::new(
            sprite_size.width / ARENA_WIDTH as f32 * window.width() as f32,
            sprite_size.height / ARENA_HEIGHT as f32 * window.height() as f32,
        );
    }
}

The sizing logic goes like so: if something has a width of 1 in a grid of 40, and the window is 400px across, then it should have a width of 10. Next we can do the positioning system:

fn position_translation(windows: Res<Windows>, mut q: Query<(&Position, &mut Transform)>) {
    fn convert(pos: f32, bound_window: f32, bound_game: f32) -> f32 {
        let tile_size = bound_window / bound_game;
        pos / bound_game * bound_window - (bound_window / 2.) + (tile_size / 2.)
    }
    let window = windows.get_primary().unwrap();
    for (pos, mut transform) in q.iter_mut() {
        transform.translation = Vec3::new(
            convert(pos.x as f32, window.width() as f32, ARENA_WIDTH as f32),
            convert(pos.y as f32, window.height() as f32, ARENA_HEIGHT as f32),
            0.0,
        );
    }
}

The position translation: if an item’s x coordinate is at 5 in our system, the width in our system is 10, and the window width is 200, then the coordinate should be 5 / 10 * 200 - 200 / 2. We subtract half the window width because our coordinate system starts at the bottom left, and Translation starts from the center. We then add half the size of a single tile, because we want our sprites bototm left corner to be at the bottom left of a tile, not the center.

Then we’ll add these systems to the app builder:

.add_system(snake_movement.system())
.add_system(position_translation.system()) <--
.add_system(size_scaling.system()) <--
.add_plugins(DefaultPlugins)
.run();

Now when you run it, you should get a squished little snake in the bottom left:

The most obvious issue here is the snake is squished. Another issue is that we've broken our input handling. We'll fix the input handling first, but rest assured we're coming back to our squished little snake to get him back to the right proportions.

Using our grid

See the diff for this section

Now that we’ve got our grid setup, we need to update our snake_movement system. Where we were using Transform before, we use Position now:

fn snake_movement(
    keyboard_input: Res<Input<KeyCode>>,
    mut head_positions: Query<With<SnakeHead, &mut Position>>,
) {
    for mut pos in head_positions.iter_mut() {
        if keyboard_input.pressed(KeyCode::Left) {
            pos.x -= 1;
        }
        if keyboard_input.pressed(KeyCode::Right) {
            pos.x += 1;
        }
        if keyboard_input.pressed(KeyCode::Down) {
            pos.y -= 1;
        }
        if keyboard_input.pressed(KeyCode::Up) {
            pos.y += 1;
        }
    }
}

Now our squished snake will follow the grid we’ve created:

Resizing the window

See the diff for this section

The reason you get a squished snake in the previous step is because the default window size isn’t square but our grid is, so each coordinate in our grid is wider than it is tall. There’s a simple fix for that, and that’s creating a WindowDescriptor resource when we build the app:

    App::build()
        .add_resource(WindowDescriptor { // <--
            title: "Snake!".to_string(), // <--
            width: 2000,                 // <--
            height: 2000,                // <--
            ..Default::default()         // <--
        })
        .add_startup_system(setup.system())

While we’re at it, let’s change the clear color (aka background color), just to make it look a bit nicer, insert this use statement to get the ClearColor struct:

use bevy::render::pass::ClearColor;

Then add it to the app builder as a resource:

.add_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04)))

Now we’re back to having a square, and now with a darker background:

Spawning food

See the diff for this section

Now that we’ve got our snake moving around a bit, let’s give it something to eat. We’ll add a new food_material field to our Materials struct first:

struct Materials {
    head_material: Handle<ColorMaterial>,
    food_material: Handle<ColorMaterial>, // <--
}

We’ll add this new material in our setup function:

commands.insert_resource(Materials {
    head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
    food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()), // <--
});

We’ll need Duration for the timer we’ll create, and we’ll need random so we can place food in random spots, so let’s use those in advance:

use rand::prelude::random;
use std::time::Duration;

Then we’ll introduce two new structs; a Food component so we know which entities are food, and a timer that will fire intermittently to tell us to spawn some food:

struct Food;

struct FoodSpawnTimer(Timer);
impl Default for FoodSpawnTimer {
    fn default() -> Self {
        Self(Timer::new(Duration::from_millis(1000), true))
    }
}

The reason for creating a Default will become clear (hopefully), when I explain the following new system:

fn food_spawner(
    mut commands: Commands,
    materials: Res<Materials>,
    time: Res<Time>,
    mut timer: Local<FoodSpawnTimer>,
) {
    timer.0.tick(time.delta_seconds);
    if timer.0.finished {
        commands
            .spawn(SpriteComponents {
                material: materials.food_material.clone(),
                ..Default::default()
            })
            .with(Food)
            .with(Position {
                x: (random::<f32>() * ARENA_WIDTH as f32) as i32,
                y: (random::<f32>() * ARENA_HEIGHT as f32) as i32,
            })
            .with(Size::square(0.8));
    }
}

We’re introducing the concept of local resources, with the timer param. Bevy will see this param and instantiate a value of type FoodSpawnTimer using our Default implementation. This will happen on the first run of the system, after that it will reuse the same timer. Using local resources in this way can be more ergonomic than registering resources manually. The timer is repeating, so we just keep calling tick and whenever the system runs when it has finished, we spawn some food randomly. Here’s what that looks like now:

You probably know what’s next, add that system to the app builder:

.add_system(food_spawner.system())

More snake-like movement

See the diff for this section

We’re going to tackle the snake movement. Specifically, we want the snake to move regardless of whether we’re currently pressing down any keys, and we want it to move every X seconds, not every frame. We’ll be making changes in quite a few areas, so if you’re not sure where something goes, check out the diff button above.

The first thing we need to add is a direction enum:

#[derive(PartialEq, Copy, Clone)]
enum Direction {
    Left,
    Up,
    Right,
    Down,
}

impl Direction {
    fn opposite(self) -> Self {
        match self {
            Self::Left => Self::Right,
            Self::Right => Self::Left,
            Self::Up => Self::Down,
            Self::Down => Self::Up,
        }
    }
}

We’ll add this direction to our SnakeHead struct, so it knows which way it’s going:

struct SnakeHead {
    direction: Direction,
}

We’ll have to instantiate the SnakeHead component with a direction now, let’s say it starts going up:

.with(SnakeHead {
    direction: Direction::Up,
})

Snake generally isn’t smooth, it’s a step-wise sort of motion. Just like for spawning food, we’re going to be using timers to get systems to run every X seconds/milliseconds. We’re going to create a struct to hold a timer:

struct SnakeMoveTimer(Timer);

Then we’ll add it as a resource in our app builder:

.add_resource(SnakeMoveTimer(Timer::new(
    Duration::from_millis(150. as u64),
    true,
)))

The reason we’re not doing this timer as a local resource like with the food spawning, is because we’re going to use it in a few systems, so I’ll save you the refactoring work. Since we’re using it in a few systems, we’re going to create a new system just to tick the timer:

fn snake_timer(time: Res<Time>, mut snake_timer: ResMut<SnakeMoveTimer>) {
    snake_timer.0.tick(time.delta_seconds);
}

We could just put this ticking logic in the snake_movement system, but I like the cleanliness of having a separate system for it, since the timer will be used in multiple places. Let’s just add that system to the app:

.add_system(snake_timer.system())

Now we can get to the meat of the direction logic, which is the snake_movement system, here’s the updated version:

fn snake_movement(
    keyboard_input: Res<Input<KeyCode>>,
    snake_timer: ResMut<SnakeMoveTimer>,
    mut heads: Query<(Entity, &mut SnakeHead)>,
    mut positions: Query<&mut Position>,
) {
    if let Some((head_entity, mut head)) = heads.iter_mut().next() {
        let mut head_pos = positions.get_mut(head_entity).unwrap();
        let dir: Direction = if keyboard_input.pressed(KeyCode::Left) {
            Direction::Left
        } else if keyboard_input.pressed(KeyCode::Down) {
            Direction::Down
        } else if keyboard_input.pressed(KeyCode::Up) {
            Direction::Up
        } else if keyboard_input.pressed(KeyCode::Right) {
            Direction::Right
        } else {
            head.direction
        };
        if dir != head.direction.opposite() {
            head.direction = dir;
        }
        if !snake_timer.0.finished {
            return;
        }
        match &head.direction {
            Direction::Left => {
                head_pos.x -= 1;
            }
            Direction::Right => {
                head_pos.x += 1;
            }
            Direction::Up => {
                head_pos.y += 1;
            }
            Direction::Down => {
                head_pos.y -= 1;
            }
        };
    }
}

There’s not much in the way of new concepts here, just game logic. You may wonder why we’re getting the Entity with a SnakeHead component, then getting the position from a separate query, instead of using something like Query<Entity, &SnakeHead, &mut Position>. The reason for that is we’ll soon need to get the position of other entities, and having two queries that access the same component isn’t allowed by the Bevy app builder. After this you’ll have a snake head that moves a bit more… snake-like:

Adding a tail

See the diff for this section

The tail of the snake is somewhat complex. For each segment, we need to know where it needs to go next. The way we’re going to approach this is to put the snake segments in a Vec, and store that as a resource. That way, when we’re updating the position of a segment, we can iterate through all the segments and set the position of each to the position of the segment before it.

Let’s add a segment_material field to our handy Materials struct:

struct Materials {
    head_material: Handle<ColorMaterial>,
    segment_material: Handle<ColorMaterial>, // <--
    food_material: Handle<ColorMaterial>,
}

Same old song and dance, add the segment_material in the setup:

commands.insert_resource(Materials {
    head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
    segment_material: materials.add(Color::rgb(0.3, 0.3, 0.3).into()), // <--
    food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()),
});

A component for our snake segments:

struct SnakeSegment;

And the resource we talked about for storing the list of segments:

#[derive(Default)]
struct SnakeSegments(Vec<Entity>);

Let’s add that as a resource in our app:

.add_resource(SnakeSegments::default()) // <--

Since we’re going to be spawning segments from a couple places (when you eat food and when you initialize the snake), we’ll create a helper function:

fn spawn_segment(
    commands: &mut Commands,
    material: &Handle<ColorMaterial>,
    position: Position,
) -> Entity {
    commands
        .spawn(SpriteComponents {
            material: material.clone(),
            ..SpriteComponents::default()
        })
        .with(SnakeSegment)
        .with(position)
        .with(Size::square(0.65))
        .current_entity()
        .unwrap()
}

This should look very similar to the spawning of the SnakeHead, but instead of a SnakeHead component, it’s got a SnakeSegment component. Something new here is that we’re then getting that Entity (which is really just an id), by using the current_entity function, and returning it so that callers can use it. Now, we’ll need to modify our game setup function. Instead of just a head, it’s also going to spawn… a snake segment (shocked pikachu meme):

fn spawn_snake(
    mut commands: Commands,
    materials: Res<Materials>,
    mut segments: ResMut<SnakeSegments>,
) {
    segments.0 = vec![
        commands
            .spawn(SpriteComponents {
                material: materials.head_material.clone(),
                ..Default::default()
            })
            .with(SnakeHead {
                direction: Direction::Up,
            })
            .with(SnakeSegment)
            .with(Position { x: 3, y: 3 })
            .with(Size::square(0.8))
            .current_entity()
            .unwrap(),
        spawn_segment(
            &mut commands,
            &materials.segment_material,
            Position { x: 3, y: 2 },
        ),
    ];
}

Our first segment is the head, which you can see now has a with(SnakeSegment) addition. Our second segment is comes from our spawn_segment function. Voila, we’ve got a detached little “tail”:

Making the tail follow the snake

See the diff for this section

One essential part of the game of snake as I remember it is that the head didn’t immediately detach from the tail. Let’s see how we can modify our snake_movement function to be more true to the original game. The first thing we need to do is add our SnakeSegments resource to the snake_movement function:

fn snake_movement(
    keyboard_input: Res<Input<KeyCode>>,
    snake_timer: ResMut<SnakeMoveTimer>,
    segments: ResMut<SnakeSegments>, // <--
    mut heads: Query<(Entity, &mut SnakeHead)>,
    mut positions: Query<&mut Position>,

Now, directly after the topmost if let, we’re going to get the positions of all the segments (which, remember, includes the head):

let segment_positions = segments
    .0
    .iter()
    .map(|e| *positions.get_mut(*e).unwrap())
    .collect::<Vec<Position>>();

Then all we need to do at the end of the if let is to iterate through the segments (skipping the head, since we’ve updated it with user input), and set the position of each segment to the position of the segment in front of it. The first tail segment gets set to the head position, second tail segment gets set to the first tail segment position, etc.

segment_positions
    .iter()
    .zip(segments.0.iter().skip(1))
    .for_each(|(pos, segment)| {
        *positions.get_mut(*segment).unwrap() = *pos;
    });

Here’s what we should have now:

Growing the snake

See the diff for this section

The snake’s been taunted by inedible food for long enough. We’re going to add a new system that lets the snake eat food:

fn snake_eating(
    mut commands: Commands,
    snake_timer: ResMut<SnakeMoveTimer>,
    mut growth_events: ResMut<Events<GrowthEvent>>,
    food_positions: Query<With<Food, (Entity, &Position)>>,
    head_positions: Query<With<SnakeHead, &Position>>,
) {
    if !snake_timer.0.finished {
        return;
    }
    for head_pos in head_positions.iter() {
        for (ent, food_pos) in food_positions.iter() {
            if food_pos == head_pos {
                commands.despawn(ent);
                growth_events.send(GrowthEvent);
            }
        }
    }
}

Just iterating through all food positions and seeing if they share a position with the head of the snake. If they do, we remove them using the handy despawn function, then trigger a GrowthEvent. Let’s create that struct:

struct GrowthEvent;

Using events is a new concept. You can send and recieve events between systems. They can be arbitrary structs so you can include any data you want in your events. For example, you may have one system that sends jump events, then a separate system that processes them. In our case, we’ll have a system that sends growth events, and a growth system to process them. You need to register events, just like we do for resources, systems, etc.:

.add_event::<GrowthEvent>()

Let’s add the snake_eating system too, while we’re here:

.add_system(snake_eating.system())

Now the snake should be able to eat food. But the snake is just a black hole for now, it doesn’t grow. One thing we need to think about for growing is that we need to know where the last segment was before moving, because that’s where the new segment goes. Let’s create that as a new resource:

#[derive(Default)]
struct LastTailPosition(Option<Position>);

And then down in the app builder:

.add_resource(LastTailPosition::default())

We need to make a couple very minor modifications to the snake_movement system, to update that LastTailPosition resource. The first is to add the resource as a param:

fn snake_movement(
    // ...
    mut last_tail_position: ResMut<LastTailPosition>, // <--
    // ...

The second is to assign that resource to the position of the last segment, this goes after we iterate through segment_positions:

last_tail_position.0 = Some(*segment_positions.last().unwrap()); // <--

After that the snake growth system is fairly straightforward:

fn snake_growth(
    mut commands: Commands,
    last_tail_position: Res<LastTailPosition>,
    growth_events: Res<Events<GrowthEvent>>,
    mut segments: ResMut<SnakeSegments>,
    mut growth_reader: Local<EventReader<GrowthEvent>>,
    materials: Res<Materials>,
) {
    if growth_reader.iter(&growth_events).next().is_some() {
        segments.0.push(spawn_segment(
            &mut commands,
            &materials.segment_material,
            last_tail_position.0.unwrap(),
        ));
    }
}

Then we’ll just add that system:

.add_system(snake_growth.system())

Hitting the wall (or our tail)

See the diff for this section

Let’s make it so that hitting walls and the snake’s tail triggers game over. We’ll use an event, like we did for growing:

struct GameOverEvent;

Now let’s register it as an event in our app builder:

.add_event::<GameOverEvent>()

In our snake_movement system, we want access to the game over event, so we can send events:

fn snake_movement(
    // ...
    mut game_over_events: ResMut<Events<GameOverEvent>>, // <--
    // ...
) {

Let’s just take the case of running into a wall first, put this right after the match &head.direction:

if head_pos.x < 0
    || head_pos.y < 0
    || head_pos.x as u32 >= ARENA_WIDTH
    || head_pos.y as u32 >= ARENA_HEIGHT
{
    game_over_events.send(GameOverEvent);
}

Alright, so we’ve got our snake_movement system sending game over events. Let’s create a new system that listens for these events:

fn game_over(
    mut commands: Commands,
    mut reader: Local<EventReader<GameOverEvent>>,
    game_over_events: Res<Events<GameOverEvent>>,
    materials: Res<Materials>,
    segments_res: ResMut<SnakeSegments>,
    food: Query<With<Food, Entity>>,
    segments: Query<With<SnakeSegment, Entity>>,
) {
    if reader.iter(&game_over_events).next().is_some() {
        for ent in food.iter().chain(segments.iter()) {
            commands.despawn(ent);
        }
        spawn_snake(commands, materials, segments_res);
    }
}

The cool thing here is that we can use the spawn_snake function directly, it’s now being used as both a system and a helper function.

For the final touch to round out our snake game, let’s add the bit of logic we need to send a game over event when we hit our own tail, inside the snake_movement system, right after the check for running out of bounds:

if segment_positions.contains(&head_pos) {
    game_over_events.send(GameOverEvent);
}

At last, we have our final result: