前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Rust: 如何用bevy写一个贪吃蛇(下)

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

作者头像
菩提树下的杨过
发布2021-12-20 21:43:15
9370
发布2021-12-20 21:43:15
举报
文章被收录于专栏:菩提树下的杨过

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

一、随机位置生成食物

代码语言:javascript
复制
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成员

代码语言:javascript
复制
...
struct Materials {
    head_material: Handle<ColorMaterial>,
    food_material: Handle<ColorMaterial>, // <--
}
代码语言:javascript
复制
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();
}

二、让蛇头能自行前进

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

代码语言:javascript
复制
#[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成员,初始化时,默认蛇头向上运动

代码语言:javascript
复制
 .insert(SnakeHead {
            direction: Direction::Up,
        })

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

代码语言:javascript
复制
/**
 *方向键改变运动方向
 */
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),综合考虑这几种状态,再引入一个枚举:

代码语言:javascript
复制
#[derive(SystemLabel, Debug, Hash, PartialEq, Eq, Clone)]
pub enum SnakeMovement {
    Input,
    Movement,
    Eating,
    Growth,
}

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

代码语言:javascript
复制
        .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标签

三、添加蛇身

代码语言:javascript
复制
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个列表。初始化的地方,相应略做调整:

代码语言:javascript
复制
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 },
        ),
    ];
}

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

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

代码语言:javascript
复制
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;
            });
    }
}

四、吃食物

代码语言:javascript
复制
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添加到系统里:

代码语言:javascript
复制
        .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个蛇身方块的位置

代码语言:javascript
复制
#[derive(Default)]
struct LastTailPosition(Option<Position>);

然后在main中添加这个资源

代码语言:javascript
复制
.insert_resource(LastTailPosition::default()) // <--

长身体的函数如下:

代码语言:javascript
复制
//蛇身长大
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

代码语言:javascript
复制
        .add_system_set(
            SystemSet::new()
                ...
                .with_system( //<--
                    snake_growth
                        .system()
                        .label(SnakeMovement::Growth)
                        .after(SnakeMovement::Eating),//Growth在吃完食物后处理
                )
        )

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

六 GameOver处理

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

代码语言:javascript
复制
struct GameOverEvent;

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

代码语言:javascript
复制
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事件后,重头来过

代码语言:javascript
复制
/**
 * 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事件加到系统中

代码语言:javascript
复制
fn main() {
    App::build()
        ...
        .add_event::<GameOverEvent>()
        ...
        .add_system(game_over.system().after(SnakeMovement::Movement))//<-- GameOver处理
        ...
        .run();
}

运行一下:

七、食物生成优化

目前还有2个小问题:

  1. 食物过多,通常1次只生成1个食物,最好是当前食物被吃掉后,才能生成下1个
  2. 食物生成的位置,目前是随机生成的,有可能生成的位置,正好在蛇身中,看上去比较奇怪。
代码语言:javascript
复制
//随机位置生成食物
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完整代码:

代码语言:javascript
复制
//基于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/

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-12-19 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档