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/