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

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

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

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

一、先搭一个"空"架子

1.1 Cargo.toml依赖项

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

代码语言:javascript
复制
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的坐标系

代码语言:javascript
复制
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 坐标系的中心。

再加点运动效果:

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

三、自定义网格坐标

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

代码语言:javascript
复制
//格子的数量(横向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,
        }
    }
}

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

代码语言:javascript
复制
//画网格辅助线
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,
    );
}

蛇头初始化的地方,相应的调整一下:

代码语言:javascript
复制
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个函数,来应用网格系统:

代码语言:javascript
复制
//根据网格大小,对方块尺寸进行缩放
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个函数加进去

代码语言:javascript
复制
        .add_system_set_to_stage( //<--
            CoreStage::PostUpdate,
            SystemSet::new()
                .with_system(position_translation.system())
                .with_system(size_scaling.system()),
        )
        .add_plugins(DefaultPlugins)

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

代码语言:javascript
复制
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 把指定函数的执行速度调慢一些。

代码语言:javascript
复制
        .add_system_set(// <--
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(1.0))
                .with_system(snake_movement.system()),
        )

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

代码语言:javascript
复制
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的完整代码如下:

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

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

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

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

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

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