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


    bevy社区有一篇不错的入门教程:Creating a Snake Clone in Rust, with Bevy,详细讲解了贪吃蛇的开发过程,我加了一些个人理解,记录于此:

    一、先搭一个"空"架子

    1.1 Cargo.toml依赖项

    [dependencies]
    bevy = { version = "0.5.0", features = ["dynamic"] }
    rand = "0.7.3"
    bevy_prototype_debug_lines = "0.3.2"
    

    贪吃蛇游戏过程中,要在随机位置生成食物,所以用到了rand,至于bevy_prototype_debug_lines这是1个画线的辅助plugin,后面在讲grid坐标转换时,可以辅助画线,更容易理解坐标系统

    1.2 main.rs

    use bevy::prelude::*;
    
    fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
        //这是1个2d游戏,所以放了一个2d"摄像机"
        let mut camera = OrthographicCameraBundle::new_2d();
        camera.transform = Transform::from_translation(Vec3::new(0.0, 0.0, 5.0));
        commands.spawn_bundle(camera);
    }
    
    fn main() {
        App::build()
            .insert_resource(WindowDescriptor {
                //窗口标题
                title: "snake".to_string(),
                //窗口大小
                 300.,
                height: 200.,
                //不允许改变窗口尺寸
                resizable: false,
                ..Default::default()
            })
            //窗口背景色
            .insert_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04)))
            .add_startup_system(setup.system())
            //默认插件
            .add_plugins(DefaultPlugins)
            .run();
    }
    

    运行起来,就得到了1个黑背景的窗口应用程序。 

    二、加入蛇头&理解bevy的坐标系

    use bevy::prelude::*;
    use bevy_prototype_debug_lines::*; //<--
    
    struct SnakeHead; //<--
    struct Materials { //<--
        head_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()),
        });
    }
    
    fn spawn_snake(mut commands: Commands, materials: Res<Materials>) { //<--
        commands
            .spawn_bundle(SpriteBundle {
                material: materials.head_material.clone(),
                //生成1个30*30px大小的2d方块
                sprite: Sprite::new(Vec2::new(30.0, 30.0)),
                ..Default::default()
            })
            .insert(SnakeHead);
    } 
    
    fn draw_center_cross(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();
        //画横线
        lines.line(
            Vec3::new(-1. * half_win_width, 0., 0.0),
            Vec3::new(half_win_width, 0., 0.0),
            0.0,
        );
    
        //画竖线
        lines.line(
            Vec3::new(0., -1. * half_win_height, 0.0),
            Vec3::new(0., half_win_height, 0.0),
            0.0,
        );
    }
    
    fn main() {
        App::build()
            .insert_resource(WindowDescriptor {
                title: "snake".to_string(),
                 300.,
                height: 200.,
                resizable: false,
                ..Default::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_center_cross.system())// <--
            .add_plugins(DefaultPlugins)
            .add_plugin(DebugLinesPlugin)// <--
            .run();
    }
    

    带<--的为新增部分,代码虽然看上去加了不少,但并不难理解,主要就是定义了1个方块充分蛇头,然后画了2根辅助线。从运行结果来看,屏幕中心就是bevy 坐标系的中心。

    再加点运动效果:

    fn snake_movement(windows: Res<Windows>, mut head_positions: Query<(&SnakeHead, &mut Transform)>) {
        for (_head, mut transform) in head_positions.iter_mut() {
            transform.translation.y += 1.;
            let window = windows.get_primary().unwrap();
            let half_win_height = 0.5 * window.height();
            if (transform.translation.y > half_win_height + 15.) {
                transform.translation.y = -1. * half_win_height - 15.;
            }
        }
    }
    
    ...
    
            .add_system(draw_center_cross.system()) 
            .add_system(snake_movement.system()) // <--
            .add_plugins(DefaultPlugins)
    

     

    三、自定义网格坐标

    贪吃蛇的游戏中,蛇头的移动往往是按一格格跳的,即相当于整个屏幕看成一个网络,蛇头每次移动一格。 先加一些相关定义:

    //格子的数量(横向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,
            }
        }
    }
    

    为了方便观察,在背景上画上网格线:

    //画网格辅助线
    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 spawn_snake(mut commands: Commands, materials: Res<Materials>) {
        commands
            .spawn_bundle(SpriteBundle {
                material: materials.head_material.clone(),
                //注:后面会根据网格大小,对方块进行缩放,所以这里的尺寸其实无效了,设置成0都行
                sprite: Sprite::new(Vec2::new(30.0, 30.0)), // <--
                ..Default::default()
            })
            .insert(SnakeHead)
            //放在第4行,第4列的位置
            .insert(Position { x: 3, y: 3 }) // <--
            //大小为网格的80%
            .insert(Size::square(0.8)); // <--
    }
    

    另外把窗口大小调整成400*400 ,同时先注释掉方块运动相关的代码,跑一下看看网格线显示是否正常:

    网络线是ok了,但是方块的大小和位置并无任何变化,接下来再写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,
            );
        }
    }
    

    在main函数里,把这2个函数加进去

            .add_system_set_to_stage( //<--
                CoreStage::PostUpdate,
                SystemSet::new()
                    .with_system(position_translation.system())
                    .with_system(size_scaling.system()),
            )
            .add_plugins(DefaultPlugins)
    

     移动方块时,就不能再按像素来移动了,而是按单元格来移动

    fn snake_movement(mut head_positions: Query<&mut Position, With<SnakeHead>>) {
        for mut pos in head_positions.iter_mut() {
            //每次向上移动1格
            pos.y += 1;
            if pos.y >= CELL_Y_COUNT as i32 {
                pos.y = 0;
            }
        }
    }
    

    大多数游戏引擎,都有所谓帧数的概念,在我的mac上,1秒大概是60帧,窗口刷新非常快(注:因为gif录制软件的原因,实际运行起来比图片中还要快。)

    可以利用 FixedTimestep 把指定函数的执行速度调慢一些。

            .add_system_set(// <--
                SystemSet::new()
                    .with_run_criteria(FixedTimestep::step(1.0))
                    .with_system(snake_movement.system()),
            )
    

    现在看上去好多了,最后再加入按键控制:

    fn snake_movement( //<--
        keyboard_input: Res<Input<KeyCode>>,
        mut head_positions: Query<&mut Position, With<SnakeHead>>,
    ) {
        for mut pos in head_positions.iter_mut() {
            if keyboard_input.pressed(KeyCode::Left) {
                if pos.x > 0 {
                    pos.x -= 1;
                }
            }
            if keyboard_input.pressed(KeyCode::Right) {
                if pos.x < CELL_X_COUNT as i32 - 1 {
                    pos.x += 1;
                }
            }
            if keyboard_input.pressed(KeyCode::Down) {
                if pos.y > 0 {
                    pos.y -= 1;
                }
            }
            if keyboard_input.pressed(KeyCode::Up) {
                if pos.y < CELL_Y_COUNT as i32 - 1 {
                    pos.y += 1;
                }
            }
        }
    }
    

    至此,main.rs的完整代码如下:

    use bevy::core::FixedTimestep;
    use bevy::prelude::*;
    use bevy_prototype_debug_lines::*;
    
    //格子的数量(横向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,
            }
        }
    }
    
    struct SnakeHead;
    struct Materials {
        head_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()),
        });
    }
    
    fn spawn_snake(mut commands: Commands, materials: Res<Materials>) {
        commands
            .spawn_bundle(SpriteBundle {
                material: materials.head_material.clone(),
                //注:后面会根据网格大小,对方块进行缩放,所以这里的尺寸其实无效了,设置成0都行
                sprite: Sprite::new(Vec2::new(30.0, 30.0)), // <--
                ..Default::default()
            })
            .insert(SnakeHead)
            //放在第4行,第4列的位置
            .insert(Position { x: 3, y: 3 }) // <--
            //大小为网格的80%
            .insert(Size::square(0.8)); // <--
    }
    
    //根据网格大小,对方块尺寸进行缩放
    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( //<--
        keyboard_input: Res<Input<KeyCode>>,
        mut head_positions: Query<&mut Position, With<SnakeHead>>,
    ) {
        for mut pos in head_positions.iter_mut() {
            if keyboard_input.pressed(KeyCode::Left) {
                if pos.x > 0 {
                    pos.x -= 1;
                }
            }
            if keyboard_input.pressed(KeyCode::Right) {
                if pos.x < CELL_X_COUNT as i32 - 1 {
                    pos.x += 1;
                }
            }
            if keyboard_input.pressed(KeyCode::Down) {
                if pos.y > 0 {
                    pos.y -= 1;
                }
            }
            if keyboard_input.pressed(KeyCode::Up) {
                if pos.y < CELL_Y_COUNT as i32 - 1 {
                    pos.y += 1;
                }
            }
        }
    }
    
    fn main() {
        App::build()
            .insert_resource(WindowDescriptor {
                title: "snake".to_string(),
                 300.,
                height: 300.,
                resizable: false,
                ..Default::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_set(
                // <--
                SystemSet::new()
                    .with_run_criteria(FixedTimestep::step(0.1))
                    .with_system(snake_movement.system()),
            )
            .add_system_set_to_stage(
                // <--
                CoreStage::PostUpdate,
                SystemSet::new()
                    .with_system(position_translation.system())
                    .with_system(size_scaling.system()),
            )
            .add_plugins(DefaultPlugins)
            .add_plugin(DebugLinesPlugin)
            .run();
    }
    

    下一篇,我们将继续实现贪吃蛇的其它功能...

    参考文章:

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

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

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

  • 相关阅读:
    delphi 使用条件编译指令
    [转] 编程之道 二
    delphi中XLSReadWrite控件的使用(1)简介
    delphi中XLSReadWrite控件的使用(2)delphi XE下安装
    墙纸自动换1.4算法分析
    Delphi中设置屏幕分辨率
    delphistringgrid另类自动向下滚屏
    【Hex 格式文件操作】一、intel hex格式文件说明
    INTEL hex文件格式
    [转]编程之道 一
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/Creating_a_Snake_Clone_in_Rust_with_Bevy_1.html
Copyright © 2020-2023  润新知