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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM