Rust: 如何用bevy写一个贪吃蛇(下)

上篇继续,贪吃蛇游戏中食物是不能缺少的,先来解决这个问题:

一、随机位置生成食物

use rand::prelude::random; 
...
struct 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));
}

然后要在Meterials中,加一项food_material成员

...
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()), // <-- 初始化时,指定食物为紫色
    });
}

...

fn main() {
    App::build()
       ...
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(2.0)) //<--2秒生成1次食物
                .with_system(food_spawner.system()),
        )
       ...
        .add_plugins(DefaultPlugins)
        .add_plugin(DebugLinesPlugin)
        .run();
}

 

二、让蛇头能自行前进

到目前为止,必须按方向键蛇头才能移动,大家都玩过这个游戏,不按键时,蛇头应该保持原来的方向继续前进,只到有按键改变运动方向,下面就来实现这一效果:

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

添加了Direction枚举以记录蛇头运动的方向,并在SnakeHead中添加了direction成员,初始化时,默认蛇头向上运动

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

按键处理和位置移动时,也要做相应调整:

/**
 *方向键改变运动方向
 */
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;
            }
        };
    }
}

这里会遇到一个问题,蛇头运动时其实有2种触发机制,1是不按键时,继续保持原来的方向运动(Movement);另1种是按键改变运动方向(Input)。再考虑到运动过程中,可能会吃掉食物(Eating),以及吃掉食物后身体长大(Growth),综合考虑这几种状态,再引入一个枚举:

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

在向bevy中add_system时,应该是input的处理要在Movement之前,即优先响应按键改变方向,这里就得使用bevy中的label机制

        .add_system(
            snake_movement_input
                .system()
                .label(SnakeMovement::Input) //按键处理,打上Input标签
                .before(SnakeMovement::Movement),//Input标签要在Movement标签前处理
        )
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(0.5))
                .with_system(snake_movement.system().label(SnakeMovement::Movement)),//给位置变化打上Movement标签

 

三、添加蛇身

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

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

先添加2个struct表示蛇身,注意蛇身方块随着不断吃食物增加,肯定不止一个,所以使用Vec<T>表示1个列表。初始化的地方,相应略做调整:

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()), // <--蛇身的颜色 
        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 {
                //蛇头方块
                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 },
        ),
    ];
}

为了方便观察,可以把位置移动相关的代码注释掉,跑起来后,大致如下:

接下来再来处理蛇身的运动跟随:

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() {
        //先取出蛇身列表中的所有Position
        let segment_positions = segments
            .0
            .iter()
            .map(|e| *positions.get_mut(*e).unwrap())
            .collect::<Vec<Position>>();
        //再找到蛇头的Position
        let mut head_pos = positions.get_mut(head_entity).unwrap();
        //根据蛇头方向,调整蛇头在网格中的位置
        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;
                }
            }
        };
        //蛇身的position跟随蛇头的position
        segment_positions
            .iter()
            .zip(segments.0.iter().skip(1))
            .for_each(|(pos, segment)| {
                *positions.get_mut(*segment).unwrap() = *pos;
            });
    }
}

 

四、吃食物

struct GrowthEvent; //吃掉食物后,蛇身长大的事件

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事件
                growth_writer.send(GrowthEvent);
            }
        }
    }
}

代码不复杂,核心逻辑就是判断蛇身与食物position是否相同,相同时视为食物被吃掉,然后对外触发事件。将snake_eating添加到系统里:

        .add_event::<GrowthEvent>()//添加事件
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(0.5))
                .with_system(snake_movement.system().label(SnakeMovement::Movement)) //给位置变化打上Movement标签
                .with_system( //<--
                    snake_eating
                        .system()
                        .label(SnakeMovement::Eating)//吃食物处理打上Eating标签
                        .after(SnakeMovement::Movement),//Eating标签在Movement后处理
                )
        )

 

五、长身体

先添加一个struct,记录最后1个蛇身方块的位置

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

然后在main中添加这个资源

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

长身体的函数如下:

//蛇身长大
fn snake_growth(
    commands: Commands,
    last_tail_position: Res<LastTailPosition>,
    mut segments: ResMut<SnakeSegments>,
    mut growth_reader: EventReader<GrowthEvent>,
    materials: Res<Materials>,
) {
    //如果收到了GrowthEvent事件
    if growth_reader.iter().next().is_some() {
        //向蛇身尾部新块1个方块
        segments.0.push(spawn_segment(
            commands,
            &materials.segment_material,
            last_tail_position.0.unwrap(),
        ));
    }
}

在add_system_set中添加snake_growth

        .add_system_set(
            SystemSet::new()
                ...
                .with_system( //<--
                    snake_growth
                        .system()
                        .label(SnakeMovement::Growth)
                        .after(SnakeMovement::Eating),//Growth在吃完食物后处理
                )
        )

现在看上去有点象贪吃蛇的样子了,不过有1个明显bug,蛇头可以从蛇身中穿过。

 

六 GameOver处理

蛇头撞到墙,或者遇到自己身体,应该GameOver,重新来过,这个同样可以供应Event完成

struct GameOverEvent;

先定义GameOverEvent事件,然后在蛇头移动中加入“边界检测”及“头遇到身体的检测”

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

        //再找到蛇头的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;
            }
        };

        //边界检测,超出则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);
        }

        //蛇头遇到蛇身,则GameOver  //<--
        if segment_positions.contains(&head_pos) {
            game_over_writer.send(GameOverEvent);
        }

        //蛇身的position跟随蛇头的position
        segment_positions
            .iter()
            .zip(segments.0.iter().skip(1))
            .for_each(|(pos, segment)| {
                *positions.get_mut(*segment).unwrap() = *pos;
            });

        ...
    }
}

收到GameOver事件后,重头来过

/**
 * game over处理
 */
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>>,
) {
    //如果收到GameOver事件
    if reader.iter().next().is_some() {
        //销毁所有食物及蛇身
        for ent in food.iter().chain(segments.iter()) {
            commands.entity(ent).despawn();
        }
        //重新初始化
        spawn_snake(commands, materials, segments_res);
    }
}

最后要把game_over以及GameOver事件加到系统中

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

运行一下:

 

 

七、食物生成优化

目前还有2个小问题:

1. 食物过多,通常1次只生成1个食物,最好是当前食物被吃掉后,才能生成下1个

2. 食物生成的位置,目前是随机生成的,有可能生成的位置,正好在蛇身中,看上去比较奇怪。

//随机位置生成食物
fn food_spawner(
    mut commands: Commands,
    materials: Res<Materials>,
    foods: Query<&Food>, //<--
    positions: Query<&Position, With<SnakeSegment>>, //<--
) {
    match foods.iter().next() {
        //当前无食物时,才生成
        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 {
                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));
        }
        _ => {}
    }
}

解决的方法也不复杂,生成前检查下:

1 当前是否已有食物,

2 生成的位置提前在蛇身的position中扫描一圈,如果已存在,再随机生成1个新位置,直到检测通过。

 

最后附上main.rs完整代码:

//基于Rust Bevy引擎的贪吃蛇游戏
//详细分析见:https://www.cnblogs.com/yjmyzz/p/Creating_a_Snake_Clone_in_Rust_with_Bevy_1.html
//by 菩提树下的杨过
use bevy::core::FixedTimestep;
use bevy::prelude::*;
use bevy_prototype_debug_lines::*;
use rand::prelude::random;

//格子的数量(横向10等分,纵向10等分,即10*10的网格)
const CELL_X_COUNT: u32 = 10;
const CELL_Y_COUNT: u32 = 10;

/**
 * 网格中的位置
 */
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
struct Position {
    x: i32,
    y: i32,
}

/**
 * 蛇头在网格中的大小
 */
struct Size {
    width: f32,
    height: f32,
}
impl Size {
    //贪吃蛇都是用方块,所以width/height均设置成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;

//随机位置生成食物
fn food_spawner(
    mut commands: Commands,
    materials: Res<Materials>,
    foods: Query<&Food>, //<--
    positions: Query<&Position, With<SnakeSegment>>, //<--
) {
    match foods.iter().next() {
        //当前无食物时,才生成
        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 {
                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()), //蛇身的颜色
        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 {
                //蛇头方块
                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 },
        ),
    ];
}

//根据网格大小,对方块尺寸进行缩放
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),
        );
    }
}

/**
 * 根据方块的position,将其放入适合的网格中
 */
fn position_translation(windows: Res<Windows>, mut q: Query<(&Position, &mut Transform)>) {
    // <--
    fn convert(pos: f32, window_size: f32, cell_count: f32) -> f32 {
        //算出每1格的大小
        let tile_size = window_size / cell_count;
        //计算最终坐标值
        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,
        );
    }
}

//画网格辅助线
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;
    }

    //画竖线
    lines.line(
        Vec3::new(0., -1. * half_win_height, 0.0),
        Vec3::new(0., half_win_height, 0.0),
        0.0,
    );
}

/**
 *方向键改变运动方向
 */
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;
        }
    }
}

struct GrowthEvent; //吃掉食物后,蛇身长大的事件

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事件
                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() {
        //先取出蛇身列表中的所有Position
        let segment_positions = segments
            .0
            .iter()
            .map(|e| *positions.get_mut(*e).unwrap())
            .collect::<Vec<Position>>();

        //再找到蛇头的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;
            }
        };

        //边界检测,超出则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);
        }

        //蛇头遇到蛇身,则GameOver
        if segment_positions.contains(&head_pos) {
            game_over_writer.send(GameOverEvent);
        }

        //蛇身的position跟随蛇头的position
        segment_positions
            .iter()
            .zip(segments.0.iter().skip(1))
            .for_each(|(pos, segment)| {
                *positions.get_mut(*segment).unwrap() = *pos;
            });

        //记录蛇身最后1个方块的位置
        last_tail_position.0 = Some(*segment_positions.last().unwrap());
    }
}

/**
 * game over处理
 */
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>>,
) {
    //如果收到GameOver事件
    if reader.iter().next().is_some() {
        //销毁所有食物及蛇身
        for ent in food.iter().chain(segments.iter()) {
            commands.entity(ent).despawn();
        }
        //重新初始化
        spawn_snake(commands, materials, segments_res);
    }
}

//蛇身长大
fn snake_growth(
    commands: Commands,
    last_tail_position: Res<LastTailPosition>,
    mut segments: ResMut<SnakeSegments>,
    mut growth_reader: EventReader<GrowthEvent>,
    materials: Res<Materials>,
) {
    //如果收到了GrowthEvent事件
    if growth_reader.iter().next().is_some() {
        //向蛇身尾部新块1个方块
        segments.0.push(spawn_segment(
            commands,
            &materials.segment_material,
            last_tail_position.0.unwrap(),
        ));
    }
}

//生成蛇身
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) //按键处理,打上Input标签
                .before(SnakeMovement::Movement), //Input标签要在Movement标签前处理
        )
        .add_event::<GrowthEvent>() //添加事件
        .add_event::<GameOverEvent>()
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(0.5))
                .with_system(snake_movement.system().label(SnakeMovement::Movement)) //给位置变化打上Movement标签
                .with_system(
                    snake_eating
                        .system()
                        .label(SnakeMovement::Eating) //吃食物处理打上Eating标签
                        .after(SnakeMovement::Movement), //Eating标签在Movement后处理
                )
                .with_system(
                    //<--
                    snake_growth
                        .system()
                        .label(SnakeMovement::Growth)
                        .after(SnakeMovement::Eating), //Growth在吃完食物后处理
                ),
        )
        .add_system(game_over.system().after(SnakeMovement::Movement)) //<-- GameOver处理
        .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();
}

  

参考文章:

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

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

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

posted @ 2021-12-19 11:46  菩提树下的杨过  阅读(449)  评论(0编辑  收藏  举报