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

posted @ 2021-12-18 17:07  菩提树下的杨过  阅读(801)  评论(0编辑  收藏  举报