Rust: how to write a greedy snake with bevy

Posted by Markto on Sun, 26 Dec 2021 01:22:34 +0100

meet Part I Go on, you can't lack food in the greedy Snake game. Let's solve this problem first:

1, Random location generated food

use rand::prelude::random; 
...
struct Food;

//Random location generated 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>() * CELL_X_COUNT as f32) as i32,
            y: (random::<f32>() * CELL_Y_COUNT as f32) as i32,
        })
        .insert(Size::square(0.6));
}

Then add a food item in materials_ Material member

...
struct Materials {
    head_material: Handle<ColorMaterial>,
    food_material: Handle<ColorMaterial>, // <--
}
fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
    let mut camera = OrthographicCameraBundle::new_2d();
    camera.transform = Transform::from_translation(Vec3::new(0.0, 0.0, 5.0));
    commands.spawn_bundle(camera);

    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()), // < -- during initialization, the specified food is purple
    });
}

...

fn main() {
    App::build()
       ...
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(2.0)) //< -- generate food once in 2 seconds
                .with_system(food_spawner.system()),
        )
       ...
        .add_plugins(DefaultPlugins)
        .add_plugin(DebugLinesPlugin)
        .run();
}

2, Let the snake head move forward on its own

So far, the snakehead can only move by pressing the direction key. Everyone has played this game. When not pressing the key, the snakehead should keep the original direction and move on. Only when there is a key to change the movement direction, the following is to achieve this effect:

#[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,
        }
    }
}

struct SnakeHead {
    direction: Direction,
}

The direction enumeration is added to record the direction of snake head movement, and the direction member is added in SnakeHead. During initialization, the snake head moves upward by default

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

The key processing and position movement shall also be adjusted accordingly:

/**
 *Direction keys change the direction of movement
 */
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
        };
        //Snakeheads cannot walk in the opposite direction, or they will eat their own bodies
        if dir != head.direction.opposite() {
            head.direction = dir;
        }
    }
}

/**
 * Adjust the position of snake head in the grid according to the direction of motion
 */
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 is a problem here. There are actually two trigger mechanisms when the snake head moves. One is to continue to move in the original direction without pressing the key; The other is to press the key to change the motion direction (Input). Considering that you may eat food during exercise and grow after eating food, consider these states comprehensively, and then introduce an enumeration:

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

Add to bevy_ System, the input should be processed before the Movement, that is, the priority should be to respond to the key to change the direction. Here, the label mechanism in bevy must be used

        .add_system(
            snake_movement_input
                .system()
                .label(SnakeMovement::Input) //Key processing and Input label
                .before(SnakeMovement::Movement),//The Input tag should be processed before the Movement tag
        )
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(0.5))
                .with_system(snake_movement.system().label(SnakeMovement::Movement)),//Label the position change with Movement

3, Add snake body

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

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

First add two struct s to represent the snake body. Note that there must be more than one snake body box with the increase of food, so use VEC < T > to represent a list. Slightly adjust the initialization:

fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
    let mut camera = OrthographicCameraBundle::new_2d();
    camera.transform = Transform::from_translation(Vec3::new(0.0, 0.0, 5.0));
    commands.spawn_bundle(camera);

    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()), // < -- the color of the snake 
        food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()), 
    });
}

fn spawn_snake(
    mut commands: Commands,
    materials: Res<Materials>,
    mut segments: ResMut<SnakeSegments>,
) {
    segments.0 = vec![
        commands
            .spawn_bundle(SpriteBundle {
                //Snake head square
                material: materials.head_material.clone(),
                sprite: Sprite::new(Vec2::new(10.0, 10.0)),
                ..Default::default()
            })
            .insert(SnakeHead {
                //Snakehead moves upward by default
                direction: Direction::Up,
            })
            .insert(SnakeSegment) //Snake body
            .insert(Position { x: 3, y: 3 })
            .insert(Size::square(0.8))
            .id(),
        spawn_segment( //Generate snake body
            commands,
            &materials.segment_material,
            //The snake's body follows the snake's head
            Position { x: 3, y: 2 },
        ),
    ];
}

To facilitate observation, you can comment out the code related to position movement. After running, it is roughly as follows:

Next, let's deal with the following movement of the snake:

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() {
        //Take out all positions in the snake body list first
        let segment_positions = segments
            .0
            .iter()
            .map(|e| *positions.get_mut(*e).unwrap())
            .collect::<Vec<Position>>();
        //Then find the Position of the snake head
        let mut head_pos = positions.get_mut(head_entity).unwrap();
        //Adjust the position of the snake head in the grid according to the snake head direction
        match &head.direction {
            Direction::Left => {
                if head_pos.x > 0 {
                    head_pos.x -= 1;
                }
            }
            Direction::Right => {
                if head_pos.x < CELL_X_COUNT as i32 - 1 {
                    head_pos.x += 1;
                }
            }
            Direction::Up => {
                if head_pos.y < CELL_Y_COUNT as i32 - 1 {
                    head_pos.y += 1;
                }
            }
            Direction::Down => {
                if head_pos.y > 0 {
                    head_pos.y -= 1;
                }
            }
        };
        //The position of the snake body follows the position of the snake head
        segment_positions
            .iter()
            .zip(segments.0.iter().skip(1))
            .for_each(|(pos, segment)| {
                *positions.get_mut(*segment).unwrap() = *pos;
            });
    }
}

4, Eat food

struct GrowthEvent; //After eating food, the snake grew up

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 {
                //When the position of the snake head is the same as that of the food, destroy the food (i.e. eat the food)
                commands.entity(ent).despawn();
                //Casually send Growth events
                growth_writer.send(GrowthEvent);
            }
        }
    }
}

The code is not complicated. The core logic is to judge whether the position of the snake is the same as that of the food. When the position is the same, it is considered that the food is eaten, and then an external event is triggered. Snap_ Adding eating to the system:

        .add_event::<GrowthEvent>()//Add event
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(0.5))
                .with_system(snake_movement.system().label(SnakeMovement::Movement)) //Label the position change with Movement
                .with_system( //<--
                    snake_eating
                        .system()
                        .label(SnakeMovement::Eating)//Food handling is labeled Eating
                        .after(SnakeMovement::Movement),//The Eating tag is processed after the Movement
                )
        )

5, Long body

First add a struct to record the position of the last snake box

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

Then add this resource in main

.insert_resource(LastTailPosition::default()) // <--

The function of the long body is as follows:

//The snake grows up
fn snake_growth(
    commands: Commands,
    last_tail_position: Res<LastTailPosition>,
    mut segments: ResMut<SnakeSegments>,
    mut growth_reader: EventReader<GrowthEvent>,
    materials: Res<Materials>,
) {
    //If the GrowthEvent event event is received
    if growth_reader.iter().next().is_some() {
        //Add a new block to the tail of the snake
        segments.0.push(spawn_segment(
            commands,
            &materials.segment_material,
            last_tail_position.0.unwrap(),
        ));
    }
}

In add_ system_ Add snake to set_ growth

        .add_system_set(
            SystemSet::new()
                ...
                .with_system( //<--
                    snake_growth
                        .system()
                        .label(SnakeMovement::Growth)
                        .after(SnakeMovement::Eating),//Growth treats after eating
                )
        )

Now it looks like a greedy snake, but there is an obvious bug that the snake head can pass through the snake.

Vi. GameOver processing

If the snake head hits a wall or encounters his own body, he should GameOver and start over again. This can also be completed by the Event

struct GameOverEvent;

First define the GameOverEvent event, and then add "boundary detection" and "detection of head encountering body" to the snake head movement

fn snake_movement(
   ...
    mut game_over_writer: EventWriter<GameOverEvent>, //<--
    ...
) {
    if let Some((head_entity, head)) = heads.iter_mut().next() {
        ...

        //Then find the Position of the snake head
        let mut head_pos = positions.get_mut(head_entity).unwrap();
        //Adjust the position of the snake head in the grid according to the snake head direction
        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;
            }
        };

        //Boundary detection, beyond which gameover//<--
        if head_pos.x < 0
            || head_pos.y < 0
            || head_pos.x as u32 >= CELL_X_COUNT
            || head_pos.y as u32 >= CELL_Y_COUNT
        {
            game_over_writer.send(GameOverEvent);
        }

        //When the snake head meets the snake body, gameover//<--
        if segment_positions.contains(&head_pos) {
            game_over_writer.send(GameOverEvent);
        }

        //The position of the snake body follows the position of the snake head
        segment_positions
            .iter()
            .zip(segments.0.iter().skip(1))
            .for_each(|(pos, segment)| {
                *positions.get_mut(*segment).unwrap() = *pos;
            });

        ...
    }
}

After receiving the GameOver event, start over

/**
 * game over handle
 */
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 a GameOver event is received
    if reader.iter().next().is_some() {
        //Destroy all food and snakes
        for ent in food.iter().chain(segments.iter()) {
            commands.entity(ent).despawn();
        }
        //Reinitialize
        spawn_snake(commands, materials, segments_res);
    }
}

Finally, put the game_over and GameOver events are added to the system

fn main() {
    App::build()
        ...
        .add_event::<GameOverEvent>()
        ...
        .add_system(game_over.system().after(SnakeMovement::Movement))//< -- gameover processing
        ...
        .run();
}

Run the following:

7, Food production optimization

There are two minor problems:

1. There are too many foods. Usually, only one food is generated at a time. It is best to generate the next food after the current food is eaten

2. At present, the location of food generation is randomly generated. The possible location is right in the snake, which looks strange.

//Random location generated food
fn food_spawner(
    mut commands: Commands,
    materials: Res<Materials>,
    foods: Query<&Food>, //<--
    positions: Query<&Position, With<SnakeSegment>>, //<--
) {
    match foods.iter().next() {
        //Generated only when there is no food currently
        None => {
            let mut x = (random::<f32>() * CELL_X_COUNT as f32) as i32;
            let mut y = (random::<f32>() * CELL_Y_COUNT as f32) as i32;
            //Loop detection, whether the randomly generated position is in the snake
            loop {
                let mut check_pos = true;
                for p in positions.iter() {
                    if p.x == x && p.y == y {
                        check_pos = false;
                        x = (random::<f32>() * CELL_X_COUNT as f32) as i32;
                        y = (random::<f32>() * CELL_Y_COUNT as f32) as i32;
                        break;
                    }
                }
                if check_pos {
                    break;
                }
            }

            commands
                .spawn_bundle(SpriteBundle {
                    material: materials.food_material.clone(),
                    ..Default::default()
                })
                .insert(Food)
                .insert(Position { x, y })
                .insert(Size::square(0.65));
        }
        _ => {}
    }
}

The solution is not complicated. Check before generation:

1. Whether there is food currently,

2. The generated position is scanned in the position of the snake body in advance. If it already exists, a new position is randomly generated until the detection passes.

Finally, attach main RS complete code:

//Snake game based on rust best engine
//For detailed analysis, see: https://www.cnblogs.com/yjmyzz/p/Creating_a_Snake_Clone_in_Rust_with_Bevy_1.html
//by Yang Guo under the bodhi tree
use bevy::core::FixedTimestep;
use bevy::prelude::*;
use bevy_prototype_debug_lines::*;
use rand::prelude::random;

//Number of grids (10 equally divided horizontally and 10 equally divided vertically, i.e. 10 * 10 grid)
const CELL_X_COUNT: u32 = 10;
const CELL_Y_COUNT: u32 = 10;

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

/**
 * The size of the snake head in the grid
 */
struct Size {
    width: f32,
    height: f32,
}
impl Size {
    //Greedy snakes use squares, so the width/height is set to x
    pub fn square(x: f32) -> Self {
        Self {
            width: x,
            height: x,
        }
    }
}

#[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,
        }
    }
}

struct SnakeHead {
    direction: Direction,
}
struct Materials {
    head_material: Handle<ColorMaterial>,
    segment_material: Handle<ColorMaterial>,
    food_material: Handle<ColorMaterial>,
}

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

struct GameOverEvent;

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

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

struct Food;

//Random location generated food
fn food_spawner(
    mut commands: Commands,
    materials: Res<Materials>,
    foods: Query<&Food>, //<--
    positions: Query<&Position, With<SnakeSegment>>, //<--
) {
    match foods.iter().next() {
        //Generated only when there is no food currently
        None => {
            let mut x = (random::<f32>() * CELL_X_COUNT as f32) as i32;
            let mut y = (random::<f32>() * CELL_Y_COUNT as f32) as i32;
            //Loop detection, whether the randomly generated position is in the snake
            loop {
                let mut check_pos = true;
                for p in positions.iter() {
                    if p.x == x && p.y == y {
                        check_pos = false;
                        x = (random::<f32>() * CELL_X_COUNT as f32) as i32;
                        y = (random::<f32>() * CELL_Y_COUNT as f32) as i32;
                        break;
                    }
                }
                if check_pos {
                    break;
                }
            }

            commands
                .spawn_bundle(SpriteBundle {
                    material: materials.food_material.clone(),
                    ..Default::default()
                })
                .insert(Food)
                .insert(Position { x, y })
                .insert(Size::square(0.65));
        }
        _ => {}
    }
}

fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
    let mut camera = OrthographicCameraBundle::new_2d();
    camera.transform = Transform::from_translation(Vec3::new(0.0, 0.0, 5.0));
    commands.spawn_bundle(camera);

    let materials = 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()), //The color of the snake
        food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()),
    };

    commands.insert_resource(materials);
}

fn spawn_snake(
    mut commands: Commands,
    materials: Res<Materials>,
    mut segments: ResMut<SnakeSegments>,
) {
    segments.0 = vec![
        commands
            .spawn_bundle(SpriteBundle {
                //Snake head square
                material: materials.head_material.clone(),
                sprite: Sprite::new(Vec2::new(10.0, 10.0)),
                ..Default::default()
            })
            .insert(SnakeHead {
                //Snakehead moves upward by default
                direction: Direction::Up,
            })
            .insert(SnakeSegment)
            .insert(Position { x: 3, y: 3 })
            .insert(Size::square(0.8))
            .id(),
        spawn_segment(
            //Generate snake body
            commands,
            &materials.segment_material,
            //The snake's body follows the snake's head
            Position { x: 3, y: 2 },
        ),
    ];
}

//Scale the box size according to the grid size
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 * (window.width() as f32 / CELL_X_COUNT as f32),
            sprite_size.height * (window.height() as f32 / CELL_Y_COUNT as f32),
        );
    }
}

/**
 * According to the position of the box, put it into the appropriate grid
 */
fn position_translation(windows: Res<Windows>, mut q: Query<(&Position, &mut Transform)>) {
    // <--
    fn convert(pos: f32, window_size: f32, cell_count: f32) -> f32 {
        //Calculate the size of each grid
        let tile_size = window_size / cell_count;
        //Calculate final coordinate value
        pos * tile_size - 0.5 * window_size + 0.5 * tile_size
    }
    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, CELL_X_COUNT as f32),
            convert(pos.y as f32, window.height() as f32, CELL_Y_COUNT as f32),
            0.0,
        );
    }
}

//Draw grid guides
fn draw_grid(windows: Res<Windows>, mut lines: ResMut<DebugLines>) {
    // <--
    let window = windows.get_primary().unwrap();
    let half_win_width = 0.5 * window.width();
    let half_win_height = 0.5 * window.height();
    let x_space = window.width() / CELL_X_COUNT as f32;
    let y_space = window.height() / CELL_Y_COUNT as f32;

    let mut i = -1. * half_win_height;
    while i < half_win_height {
        lines.line(
            Vec3::new(-1. * half_win_width, i, 0.0),
            Vec3::new(half_win_width, i, 0.0),
            0.0,
        );
        i += y_space;
    }

    i = -1. * half_win_width;
    while i < half_win_width {
        lines.line(
            Vec3::new(i, -1. * half_win_height, 0.0),
            Vec3::new(i, half_win_height, 0.0),
            0.0,
        );
        i += x_space;
    }

    //Draw a vertical line
    lines.line(
        Vec3::new(0., -1. * half_win_height, 0.0),
        Vec3::new(0., half_win_height, 0.0),
        0.0,
    );
}

/**
 *Direction keys change the direction of movement
 */
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
        };
        //Snakeheads cannot walk in the opposite direction, or they will eat their own bodies
        if dir != head.direction.opposite() {
            head.direction = dir;
        }
    }
}

struct GrowthEvent; //After eating food, the snake grew up

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 {
                //When the position of the snake head is the same as that of the food, destroy the food (i.e. eat the food)
                commands.entity(ent).despawn();
                //Casually send Growth events
                growth_writer.send(GrowthEvent);
            }
        }
    }
}

fn snake_movement(
    segments: ResMut<SnakeSegments>,
    mut heads: Query<(Entity, &SnakeHead)>,
    mut last_tail_position: ResMut<LastTailPosition>,
    mut game_over_writer: EventWriter<GameOverEvent>,
    mut positions: Query<&mut Position>,
) {
    if let Some((head_entity, head)) = heads.iter_mut().next() {
        //Take out all positions in the snake body list first
        let segment_positions = segments
            .0
            .iter()
            .map(|e| *positions.get_mut(*e).unwrap())
            .collect::<Vec<Position>>();

        //Then find the Position of the snake head
        let mut head_pos = positions.get_mut(head_entity).unwrap();
        //Adjust the position of the snake head in the grid according to the snake head direction
        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;
            }
        };

        //Boundary detection, beyond which GameOver
        if head_pos.x < 0
            || head_pos.y < 0
            || head_pos.x as u32 >= CELL_X_COUNT
            || head_pos.y as u32 >= CELL_Y_COUNT
        {
            game_over_writer.send(GameOverEvent);
        }

        //When the snake head meets the snake body, GameOver
        if segment_positions.contains(&head_pos) {
            game_over_writer.send(GameOverEvent);
        }

        //The position of the snake body follows the position of the snake head
        segment_positions
            .iter()
            .zip(segments.0.iter().skip(1))
            .for_each(|(pos, segment)| {
                *positions.get_mut(*segment).unwrap() = *pos;
            });

        //Record the position of the last square of the snake
        last_tail_position.0 = Some(*segment_positions.last().unwrap());
    }
}

/**
 * game over handle
 */
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 a GameOver event is received
    if reader.iter().next().is_some() {
        //Destroy all food and snakes
        for ent in food.iter().chain(segments.iter()) {
            commands.entity(ent).despawn();
        }
        //Reinitialize
        spawn_snake(commands, materials, segments_res);
    }
}

//The snake grows up
fn snake_growth(
    commands: Commands,
    last_tail_position: Res<LastTailPosition>,
    mut segments: ResMut<SnakeSegments>,
    mut growth_reader: EventReader<GrowthEvent>,
    materials: Res<Materials>,
) {
    //If the GrowthEvent event event is received
    if growth_reader.iter().next().is_some() {
        //Add a new block to the tail of the snake
        segments.0.push(spawn_segment(
            commands,
            &materials.segment_material,
            last_tail_position.0.unwrap(),
        ));
    }
}

//Generate snake body
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()
}

fn main() {
    App::build()
        .insert_resource(WindowDescriptor {
            title: "snake".to_string(),
            width: 300.,
            height: 300.,
            resizable: false,
            ..Default::default()
        })
        .insert_resource(LastTailPosition::default()) // <--
        .insert_resource(SnakeSegments::default())
        .insert_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04)))
        .add_startup_system(setup.system())
        .add_startup_stage("game_setup", SystemStage::single(spawn_snake.system()))
        .add_system(draw_grid.system())
        .add_system(
            snake_movement_input
                .system()
                .label(SnakeMovement::Input) //Key processing and Input label
                .before(SnakeMovement::Movement), //The Input tag should be processed before the Movement tag
        )
        .add_event::<GrowthEvent>() //Add event
        .add_event::<GameOverEvent>()
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(0.5))
                .with_system(snake_movement.system().label(SnakeMovement::Movement)) //Label the position change with Movement
                .with_system(
                    snake_eating
                        .system()
                        .label(SnakeMovement::Eating) //Food handling is labeled Eating
                        .after(SnakeMovement::Movement), //The Eating tag is processed after the Movement
                )
                .with_system(
                    //<--
                    snake_growth
                        .system()
                        .label(SnakeMovement::Growth)
                        .after(SnakeMovement::Eating), //Growth treats after eating
                ),
        )
        .add_system(game_over.system().after(SnakeMovement::Movement)) //< -- gameover processing
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(2.0))
                .with_system(food_spawner.system()),
        )
        .add_system_set_to_stage(
            CoreStage::PostUpdate,
            SystemSet::new()
                .with_system(position_translation.system())
                .with_system(size_scaling.system()),
        )
        .add_plugins(DefaultPlugins)
        .add_plugin(DebugLinesPlugin)
        .run();
}

Reference article:

https://bevyengine.org/learn/book/getting-started/

https://mbuffett.com/posts/bevy-snake-tutorial/

https://bevy-cheatbook.github.io/