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.5.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_bundle(OrthographicCameraBundle::new_2d());
}

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_bundle(OrthographicCameraBundle::new_2d());
    commands.insert_resource(Materials {
        head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
    });
}

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_bundle(SpriteBundle {
            material: materials.head_material.clone(),
            sprite: Sprite::new(Vec2::new(10.0, 10.0)),
            ..Default::default()
        })
        .insert(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 SpriteBundle and SnakeHead as components. To create the SpriteBundle, 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", SystemStage::single(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. SystemStage::single creates a new stage with only one system. You could also use serial or parallel, if you have multiple systems in a stage, but here we just have one. After running this, you should see a snake head in the middle of the screen:

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 += 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_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 SpriteBundle is a Bundle of components. For SpriteBundle, 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<&mut Transform, With<SnakeHead>>,
) {
    for mut transform in head_positions.iter_mut() {
        if keyboard_input.pressed(KeyCode::Left) {
            transform.translation.x -= 2.;
        }
        if keyboard_input.pressed(KeyCode::Right) {
            transform.translation.x += 2.;
        }
        if keyboard_input.pressed(KeyCode::Down) {
            transform.translation.y -= 2.;
        }
        if keyboard_input.pressed(KeyCode::Up) {
            transform.translation.y += 2.;
        }
    }
}

Worth noting here is that we’ve changed the query from Query<&SnakeHead, &mut Transform>, to Query<&mut Transform, With<SnakeHead>>. 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_bundle(SpriteBundle {
        material: materials.head_material.clone(),
        sprite: Sprite::new(Vec2::new(10.0, 10.0)),
        ..Default::default()
    })
    .insert(SnakeHead)
    .insert(Position { x: 3, y: 3 }) // <--
    .insert(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 bottom left corner to be at the bottom left of a tile, not the center.

Then we’ll add these systems to the app builder. These are a bit of a special case; they should run in the PostUpdate stage. They need to be in a separate stage because commands only get executed after each stage, so if we add a new entity in our Update stage, we need that stage to finish before position_translation and size_scaling will be able to see that entity. Here’s what setting that up looks like:

.add_system(snake_movement.system())
.add_system_set_to_stage(
    CoreStage::PostUpdate,
    SystemSet::new()
        .with_system(position_translation.system())
        .with_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<&mut Position, With<SnakeHead>>,
) {
    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()
        .insert_resource(WindowDescriptor { // <--
            title: "Snake!".to_string(), // <--
            width: 500.0,                 // <--
            height: 500.0,                // <--
            ..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:

.insert_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 random so we can place food in random spots, so let’s use that in advance. We’ll also need FixedTimestep, so go ahead and put that in now so you don’t have to later:

use rand::prelude::random;
use bevy::core::FixedTimestep;

Then we’ll create a new tag struct, a Food component so we know which entities are food:

struct Food;

We’ll then need a system to spawn food:

fn food_spawner(
    mut commands: Commands,
    materials: Res<Materials>,
) {
    commands
        .spawn_bundle(SpriteBundle {
            material: materials.food_material.clone(),
            ..Default::default()
        })
        .insert(Food)
        .insert(Position {
            x: (random::<f32>() * ARENA_WIDTH as f32) as i32,
            y: (random::<f32>() * ARENA_HEIGHT as f32) as i32,
        })
        .insert(Size::square(0.8));
}

You probably know what’s next, adding that system to the app builder. But there’s a catch this time. We don’t want this going off constantly. We want to spawn food every second, not every frame. Since this is a common need in game development, to want something to happen at a fixed timestep, bevy provides the ultra-convenient FixedTimestep run criteria. Here’s what using that looks like:

.add_system_set(
    SystemSet::new()
        .with_run_criteria(FixedTimestep::step(1.0))
        .with_system(food_spawner.system()),
)

Somewhat self-explanatory, we’re adding a new set of systems (although there’s only one), which will run at a fixed timestep, in this case every 1 second.

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. We’ll also be tackling system ordering in this section, since we’ve neglected it so far.

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:

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

There’s a bit of a problem with our current system setup , because we’re asking for input in the same system as we’re moving our snake. So here’s the goal: we want to split up the input handling from the movement, and make sure the movement happens at a fixed timestep.

To accomplish this we’re introducing the concept of system labels. Here are our labels in advance. There are some that won’t be applicable yet, but it doesn’t hurt to have them there so you don’t have to go back and add them later as we add more logic.

#[derive(SystemLabel, Debug, Hash, PartialEq, Eq, Clone)]
pub enum SnakeMovement {
    Input,
    Movement,
    Eating,
    Growth,
}

Any type that implements SystemLabel can be used for labeling. Here we’re defining our own enum and letting bevy derive SystemLabel for us. Now we’re going to use these labels to order our snake systems, so go ahead and replace the add_system(snake_movement.system()) line with the below:

.add_system(
    snake_movement_input
        .system()
        .label(SnakeMovement::Input)
        .before(SnakeMovement::Movement),
)
.add_system_set(
    SystemSet::new()
        .with_run_criteria(FixedTimestep::step(0.150))
        .with_system(snake_movement.system().label(SnakeMovement::Movement)),
)

There’s a few new things here. .label(x) tags a system (or system set) with a label. On its own this does nothing, but the gain is that you can then use .before(x) or .after(x) on other systems, to specify order. So here we’re tagging the snake_movement system with .label(SnakeMovement::Movement), and for the input system we’re adding .before(SnakeMovement::Movement), to ensure that on a given frame, we get the user input before we move the snake.

You’ll notice we also used FixedTimestep here, like for spawning food.

Now we have to actually implement the snake_movement_input and snake_movement functions. Here are those, I won’t go over them because it’s just pieces of the old snake_movement function extracted into a new system.

fn snake_movement_input(keyboard_input: Res<Input<KeyCode>>, mut heads: Query<&mut SnakeHead>) {
    if let Some(mut head) = heads.iter_mut().next() {
        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;
        }
    }
}

fn snake_movement(mut heads: Query<(&mut Position, &SnakeHead)>) {
    if let Some((mut head_pos, head)) = heads.iter_mut().next() {
        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 some boilerplate direction logic. 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:

.insert_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(
    mut commands: Commands,
    material: &Handle<ColorMaterial>,
    position: Position,
) -> Entity {
    commands
        .spawn_bundle(SpriteBundle {
            material: material.clone(),
            ..Default::default()
        })
        .insert(SnakeSegment)
        .insert(position)
        .insert(Size::square(0.65))
        .id()
}

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 id 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_bundle(SpriteBundle {
                material: materials.head_material.clone(),
                sprite: Sprite::new(Vec2::new(10.0, 10.0)),
                ..Default::default()
            })
            .insert(SnakeHead {
                direction: Direction::Up,
            })
            .insert(SnakeSegment)
            .insert(Position { x: 3, y: 3 })
            .insert(Size::square(0.8))
            .id(),
        spawn_segment(
            commands,
            &materials.segment_material,
            Position { x: 3, y: 2 },
        ),
    ];
}

Our first segment is the head, which you can see now has a .insert(SnakeSegment) addition. Our second segment 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. We’ll need to change how we get the position of the head, because we can’t have two queries that are competing for Position components, and we’ll need to have access to our SnakeSegments resource. Here’s our new function:

fn snake_movement(
    segments: ResMut<SnakeSegments>,
    mut heads: Query<(Entity, &SnakeHead)>,
    mut positions: Query<&mut Position>,
) {
    if let Some((head_entity, head)) = heads.iter_mut().next() {
        let segment_positions = segments
            .0
            .iter()
            .map(|e| *positions.get_mut(*e).unwrap())
            .collect::<Vec<Position>>();
        let mut head_pos = positions.get_mut(head_entity).unwrap();
        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;
            }
        };
        segment_positions
            .iter()
            .zip(segments.0.iter().skip(1))
            .for_each(|(pos, segment)| {
                *positions.get_mut(*segment).unwrap() = *pos;
            });
    }
}

There’s a lot going on here. We’re getting the Entity of the snake head this time, instead of getting its position from a Query. Then we use positions.get_mut(head_entity).unwrap(), to get the Position of the head. The segment positions are retrieved in a similar manner, by just iterating over the segments we have in the SnakeSegments resource, and getting the Position for each one, from the positions query.

After we change the head position, we just need to 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. There’s some fun iterator magic in there, but it’s not all that bevy-specific so I’m not going to spend too much time on it.

Here’s what we should have now:

Growing the snake

See the diff for this section

The snake has 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,
    mut growth_writer: EventWriter<GrowthEvent>,
    food_positions: Query<(Entity, &Position), With<Food>>,
    head_positions: Query<&Position, With<SnakeHead>>,
) {
    for head_pos in head_positions.iter() {
        for (ent, food_pos) in food_positions.iter() {
            if food_pos == head_pos {
                commands.entity(ent).despawn();
                growth_writer.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, chained onto the SystemSet that has our snake_movement system, it should look like this:

SystemSet::new()
    .with_run_criteria(FixedTimestep::step(0.150))
    .with_system(snake_movement.system().label(SnakeMovement::Movement))
    .with_system(
        snake_eating
            .system()
            .label(SnakeMovement::Eating)
            .after(SnakeMovement::Movement),
    )

We want the eating check to happen after movement, so we add .after(SnakeMovement::Movement) so bevy makes it happen in the right order.

Now the snake should be able to eat food. But it’s 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:

.insert_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(
    commands: Commands,
    last_tail_position: Res<LastTailPosition>,
    mut segments: ResMut<SnakeSegments>,
    mut growth_reader: EventReader<GrowthEvent>,
    materials: Res<Materials>,
) {
    if growth_reader.iter().next().is_some() {
        segments.0.push(spawn_segment(
            commands,
            &materials.segment_material,
            last_tail_position.0.unwrap(),
        ));
    }
}

Then we’ll just add that system onto the same SystemSet with movement and eating, and make sure it runs after eating:

.with_system(
    snake_growth
        .system()
        .label(SnakeMovement::Growth)
        .after(SnakeMovement::Eating),
)

Hitting the wall (or our tail)

See the diff for this section

A pretty important part of the game of snake (or any game), is a losing condition. In snake that’s running into your own tail or running into the wall (some newfangled versions wrap the snake around the screen, but that’s not the version I played on my dad’s Nokia, so it’s not proper snake as far as I’m concerned).

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_writer: EventWriter<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_writer.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: EventReader<GameOverEvent>,
    materials: Res<Materials>,
    segments_res: ResMut<SnakeSegments>,
    food: Query<Entity, With<Food>>,
    segments: Query<Entity, With<SnakeSegment>>,
) {
    if reader.iter().next().is_some() {
        for ent in food.iter().chain(segments.iter()) {
            commands.entity(ent).despawn();
        }
        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_writer.send(GameOverEvent);
}

And then we’ll add game_over system, and make sure it runs after Movement:

.add_system(game_over.system().after(SnakeMovement::Movement))

At last, we have our final result:

Homework

There are two bugs with the game currently.

The first is that you can turn the snake 180°. For example, if the snake is moving right, if you press up on one frame then left on the next frame, the snake will turn around and hit itself.

The second bug is that food can spawn on top of the snake’s tail.

Homework hint/spoiler:

  • For the turning problem, part of the problem is that “the direction the snake is moving” and “the last user input”, are stored in the same variable. Being able to keep track of these independently will help.