bevy community has a good introductory tutorial: Creating a Snake Clone in Rust, with Bevy , I explained the development process of greedy snake in detail. I added some personal understanding and recorded it here:
1, Build an "empty" shelf first
1.1 Cargo.toml dependency
[dependencies] bevy = { version = "0.5.0", features = ["dynamic"] } rand = "0.7.3" bevy_prototype_debug_lines = "0.3.2"
During the snake game, food is generated at random, so rand is used, and bevy is used_ prototype_ debug_ Lines this is an auxiliary plugin for drawing lines. Later, when talking about grid coordinate conversion, it can assist in drawing lines to make it easier to understand the coordinate system
1.2 main.rs
use bevy::prelude::*; fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) { //This is a 2d game, so we put a 2d "camera" 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 { //Window title title: "snake".to_string(), //Window size width: 300., height: 200., //Changing the window size is not allowed resizable: false, ..Default::default() }) //Window background color .insert_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04))) .add_startup_system(setup.system()) //Default plug-in .add_plugins(DefaultPlugins) .run(); }

Run up and get a window application with a black background.
2, Join snakehead & understand bevy's coordinate system
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(), //Generate a 2d block of 30*30px size 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(); //Draw a horizontal line lines.line( Vec3::new(-1. * half_win_width, 0., 0.0), Vec3::new(half_win_width, 0., 0.0), 0.0, ); //Draw a vertical line 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(); }

The part with < -- is a new part. Although the code seems to have added a lot, it is not difficult to understand. It mainly defines a square full snake head, and then draws two auxiliary lines. From the running results, the center of the screen is the center of the bevy coordinate system.

Add some movement effect:
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)

3, Custom grid coordinates
In the game of greedy snake, the movement of snake head often jumps according to one grid, that is, the whole screen is regarded as a network, and the snake head moves one grid at a time. Add some related definitions first:
//Number of grids (10 equally divided horizontally and 10 equally divided vertically, i.e. 10 * 10 grid) const CELL_X_COUNT: u32 = 10; const CELL_Y_COUNT: u32 = 10; /** * Position in Grid */ #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] struct Position { x: i32, y: i32, } /** * The size of the snake head in the grid */ struct Size { width: f32, height: f32, } impl Size { //Greedy snakes use squares, so the width/height is set to x pub fn square(x: f32) -> Self { Self { width: x, height: x, } } }
To facilitate observation, draw a grid line on the background:
//Draw grid guides 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; } //Draw a vertical line lines.line( Vec3::new(0., -1. * half_win_height, 0.0), Vec3::new(0., half_win_height, 0.0), 0.0, ); }
Where snakehead is initialized, adjust it accordingly:
fn spawn_snake(mut commands: Commands, materials: Res<Materials>) { commands .spawn_bundle(SpriteBundle { material: materials.head_material.clone(), //Note: the box will be scaled according to the grid size later, so the size here is actually invalid. Set it to 0 sprite: Sprite::new(Vec2::new(30.0, 30.0)), // <-- ..Default::default() }) .insert(SnakeHead) //In row 4, column 4 .insert(Position { x: 3, y: 3 }) // <-- //80% of grid size .insert(Size::square(0.8)); // <-- }
In addition, adjust the window size to 400 * 400. At the same time, comment out the code related to square motion, and run to see whether the grid line display is normal:

The network line is ok, but there is no change in the size and position of the box. Next, write two functions to apply the grid system:
//Scale the box size according to the grid size 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), ); } } /** * According to the position of the box, put it into the appropriate grid */ fn position_translation(windows: Res<Windows>, mut q: Query<(&Position, &mut Transform)>) { // <-- fn convert(pos: f32, window_size: f32, cell_count: f32) -> f32 { //Calculate the size of each grid let tile_size = window_size / cell_count; //Calculate final coordinate value 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, ); } }
In the main function, add these two functions
.add_system_set_to_stage( //<-- CoreStage::PostUpdate, SystemSet::new() .with_system(position_translation.system()) .with_system(size_scaling.system()), ) .add_plugins(DefaultPlugins)

When you move a box, you can no longer move by pixel, but by cell
fn snake_movement(mut head_positions: Query<&mut Position, With<SnakeHead>>) { for mut pos in head_positions.iter_mut() { //Move up 1 grid at a time pos.y += 1; if pos.y >= CELL_Y_COUNT as i32 { pos.y = 0; } } }
Most game engines have the so-called concept of frame number. On my mac, one second is about 60 frames, and the window refresh is very fast (Note: due to gif recording software, it actually runs faster than in the picture.)


You can use FixedTimestep to slow down the execution of the specified function.
.add_system_set(// <-- SystemSet::new() .with_run_criteria(FixedTimestep::step(1.0)) .with_system(snake_movement.system()), )

It looks much better now. Finally, add key control:
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; } } } }

So far, main The complete code of RS is as follows:
use bevy::core::FixedTimestep; use bevy::prelude::*; use bevy_prototype_debug_lines::*; //Number of grids (10 equally divided horizontally and 10 equally divided vertically, i.e. 10 * 10 grid) const CELL_X_COUNT: u32 = 10; const CELL_Y_COUNT: u32 = 10; /** * Position in Grid */ #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] struct Position { x: i32, y: i32, } /** * The size of the snake head in the grid */ struct Size { width: f32, height: f32, } impl Size { //Greedy snakes use squares, so the width/height is set to 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(), //Note: the box will be scaled according to the grid size later, so the size here is actually invalid. Set it to 0 sprite: Sprite::new(Vec2::new(30.0, 30.0)), // <-- ..Default::default() }) .insert(SnakeHead) //In row 4, column 4 .insert(Position { x: 3, y: 3 }) // <-- //80% of grid size .insert(Size::square(0.8)); // <-- } //Scale the box size according to the grid size 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), ); } } /** * According to the position of the box, put it into the appropriate grid */ fn position_translation(windows: Res<Windows>, mut q: Query<(&Position, &mut Transform)>) { // <-- fn convert(pos: f32, window_size: f32, cell_count: f32) -> f32 { //Calculate the size of each grid let tile_size = window_size / cell_count; //Returns the final coordinate position 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, ); } } //Draw grid guides 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; } //Draw a vertical line 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(); }
Next , we will continue to realize other functions of greedy snake
Reference article:
https://bevyengine.org/learn/book/getting-started/