• 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 {
         f32,
        height: f32,
    }
    impl Size {
        //贪吃蛇都是用方块,所以width/height均设置成x
        pub fn square(x: f32) -> Self {
            Self {
                 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(),
                 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/

  • 相关阅读:
    Hibernate sqlserver 的对象转成 Hibernate mysql 的对象时 需注意
    将绿色版Tomcat服务添加到系统服务并设为开机运行
    进程上下文和中断上下文
    关于上、下拉电阻的总结整理
    I2C设备驱动流程
    MTK6573的LDO控制
    iomem—I/O映射方式的I/O端口和内存映射方式的I/O端口
    Linux I2C子系统分析I2C总线驱动
    Camera读取ID方法总结
    Linux 信号signal处理机制
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/Creating_a_Snake_Clone_in_Rust_with_Bevy_2.html
Copyright © 2020-2023  润新知