Created
May 12, 2025 13:22
-
-
Save blogscot/b00a301b043934f2da93537bdc4c0197 to your computer and use it in GitHub Desktop.
Snake Game using Bevy v0.16.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Original Snake Tutorial: Creating a Snake Clone in Rust, with Bevy [0.7.0] | |
// https://mbuffett.com/posts/bevy-snake-tutorial/ | |
use bevy::{prelude::*, time::common_conditions::on_timer, window::PrimaryWindow}; | |
use rand::random; | |
use std::time::Duration; | |
const SNAKE_HEAD_COLOR: Color = Color::srgb(0.7, 0.7, 0.7); | |
const SNAKE_SEGMENT_COLOR: Color = Color::srgb(0.3, 0.3, 0.3); | |
const FOOD_COLOR: Color = Color::srgb(1.0, 0.0, 1.0); | |
const ARENA_WIDTH: u32 = 25; | |
const ARENA_HEIGHT: u32 = 25; | |
// Resources | |
#[derive(Resource, Default)] | |
struct SnakeSegments(Vec<Entity>); | |
#[derive(Resource, Default)] | |
struct LastTailPosition(Option<Position>); | |
// Events | |
#[derive(Event)] | |
struct GrowthEvent; | |
#[derive(Event)] | |
struct GameOverEvent; | |
// Components | |
#[derive(Component)] | |
struct SnakeHead { | |
direction: Direction, | |
} | |
#[derive(Component)] | |
struct SnakeSegment; | |
#[derive(Component)] | |
struct Food; | |
#[derive(Component, Clone, Copy, PartialEq, Eq)] | |
struct Position { | |
x: i32, | |
y: i32, | |
} | |
#[derive(Component)] | |
struct Size { | |
width: f32, | |
height: f32, | |
} | |
impl Size { | |
pub fn square(x: f32) -> Self { | |
Self { | |
width: x, | |
height: x, | |
} | |
} | |
} | |
#[derive(Debug, Copy, Clone, Eq, PartialEq)] | |
enum Direction { | |
Left, | |
Up, | |
Right, | |
Down, | |
} | |
impl Direction { | |
fn opposite(self) -> Self { | |
match self { | |
Self::Left => Self::Right, | |
Self::Right => Self::Left, | |
Self::Up => Self::Down, | |
Self::Down => Self::Up, | |
} | |
} | |
} | |
fn main() { | |
App::new() | |
.insert_resource(ClearColor(Color::srgb(0.04, 0.04, 0.04))) | |
.init_resource::<SnakeSegments>() | |
.init_resource::<LastTailPosition>() | |
.add_event::<GrowthEvent>() | |
.add_event::<GameOverEvent>() | |
.add_systems(Startup, (setup_camera, spawn_snake)) | |
.add_systems(Update, snake_movement_input.before(snake_movement)) | |
.add_systems( | |
Update, | |
(snake_movement, game_over, snake_eating, snake_growth) | |
.chain() | |
.run_if(on_timer(Duration::from_secs_f32(0.25))), | |
) | |
.add_systems( | |
Update, | |
food_spawner.run_if(on_timer(Duration::from_secs_f32(4.0))), | |
) | |
.add_systems(PostUpdate, (size_scaling, position_translation)) | |
.add_plugins(DefaultPlugins.set(WindowPlugin { | |
primary_window: Some(Window { | |
title: "Snake Game".into(), | |
resolution: (500.0, 500.0).into(), | |
..default() | |
}), | |
..default() | |
})) | |
.add_systems(Update, close_on_esc) | |
.run(); | |
} | |
fn setup_camera(mut commands: Commands) { | |
commands.spawn(Camera2d); | |
} | |
fn spawn_snake(mut commands: Commands, mut segments: ResMut<SnakeSegments>) { | |
*segments = SnakeSegments(vec![ | |
commands | |
.spawn((Sprite { | |
color: SNAKE_HEAD_COLOR, | |
..default() | |
},)) | |
.insert(SnakeHead { | |
direction: Direction::Up, | |
}) | |
.insert(SnakeSegment) | |
.insert(Position { x: 3, y: 3 }) | |
.insert(Size::square(0.8)) | |
.id(), | |
spawn_segment(commands, Position { x: 3, y: 2 }), | |
]); | |
} | |
fn spawn_segment(mut commands: Commands, position: Position) -> Entity { | |
commands | |
.spawn(Sprite { | |
color: SNAKE_SEGMENT_COLOR, | |
..default() | |
}) | |
.insert(SnakeSegment) | |
.insert(position) | |
.insert(Size::square(0.65)) | |
.id() | |
} | |
fn snake_movement_input( | |
keyboard_input: Res<ButtonInput<KeyCode>>, | |
mut heads: Query<&mut SnakeHead>, | |
) { | |
if let Some(mut head) = heads.iter_mut().next() { | |
let dir: Direction = if keyboard_input.pressed(KeyCode::ArrowLeft) { | |
Direction::Left | |
} else if keyboard_input.pressed(KeyCode::ArrowDown) { | |
Direction::Down | |
} else if keyboard_input.pressed(KeyCode::ArrowUp) { | |
Direction::Up | |
} else if keyboard_input.pressed(KeyCode::ArrowRight) { | |
Direction::Right | |
} else { | |
head.direction | |
}; | |
if dir != head.direction.opposite() { | |
head.direction = dir; | |
} | |
} | |
} | |
fn snake_movement( | |
segments: ResMut<SnakeSegments>, | |
mut heads: Query<(Entity, &SnakeHead)>, | |
mut positions: Query<&mut Position>, | |
mut last_tail_position: ResMut<LastTailPosition>, | |
mut game_over_writer: EventWriter<GameOverEvent>, | |
) { | |
if let Some((head_entity, head)) = heads.iter_mut().next() { | |
let segment_positions = segments | |
.0 | |
.iter() | |
.map(|e| *positions.get_mut(*e).unwrap()) | |
.collect::<Vec<Position>>(); | |
let mut head_pos = positions.get_mut(head_entity).unwrap(); | |
match head.direction { | |
Direction::Left => { | |
head_pos.x -= 1; | |
} | |
Direction::Right => { | |
head_pos.x += 1; | |
} | |
Direction::Up => { | |
head_pos.y += 1; | |
} | |
Direction::Down => { | |
head_pos.y -= 1; | |
} | |
}; | |
if head_pos.x < 0 | |
|| head_pos.y < 0 | |
|| head_pos.x as u32 >= ARENA_WIDTH | |
|| head_pos.y as u32 >= ARENA_HEIGHT | |
{ | |
game_over_writer.write(GameOverEvent); | |
} | |
if segment_positions.contains(&head_pos) { | |
game_over_writer.write(GameOverEvent); | |
} | |
segment_positions | |
.iter() | |
.zip(segments.0.iter().skip(1)) | |
.for_each(|(pos, segment)| { | |
*positions.get_mut(*segment).unwrap() = *pos; | |
}); | |
*last_tail_position = LastTailPosition(Some(*segment_positions.last().unwrap())); | |
} | |
} | |
fn snake_eating( | |
mut commands: Commands, | |
mut growth_writer: EventWriter<GrowthEvent>, | |
food_positions: Query<(Entity, &Position), With<Food>>, | |
head_positions: Query<&Position, With<SnakeHead>>, | |
) { | |
for head_pos in head_positions.iter() { | |
for (ent, food_pos) in food_positions.iter() { | |
if food_pos == head_pos { | |
commands.entity(ent).despawn(); | |
growth_writer.write(GrowthEvent); | |
break; | |
} | |
} | |
} | |
} | |
fn snake_growth( | |
commands: Commands, | |
last_tail_position: Res<LastTailPosition>, | |
mut segments: ResMut<SnakeSegments>, | |
mut growth_reader: EventReader<GrowthEvent>, | |
) { | |
if growth_reader.read().next().is_some() { | |
segments | |
.0 | |
.push(spawn_segment(commands, last_tail_position.0.unwrap())); | |
} | |
} | |
fn food_spawner(mut commands: Commands, positions: Query<&mut Position>) { | |
// Prevent food from spawning on top of snake | |
let mut x = (random::<f32>() * ARENA_WIDTH as f32) as i32; | |
let mut y = (random::<f32>() * ARENA_HEIGHT as f32) as i32; | |
while positions.iter().any(|pos| pos.x == x && pos.y == y) { | |
x = (random::<f32>() * ARENA_WIDTH as f32) as i32; | |
y = (random::<f32>() * ARENA_HEIGHT as f32) as i32; | |
} | |
commands | |
.spawn(Sprite { | |
color: FOOD_COLOR, | |
..default() | |
}) | |
.insert(Food) | |
.insert(Position { x, y }) | |
.insert(Size::square(0.8)); | |
} | |
fn size_scaling( | |
primary_window: Single<&Window, With<PrimaryWindow>>, | |
mut q: Query<(&Size, &mut Transform)>, | |
) { | |
let window = primary_window; | |
for (sprite_size, mut transform) in q.iter_mut() { | |
transform.scale = Vec3::new( | |
sprite_size.width / ARENA_WIDTH as f32 * window.width(), | |
sprite_size.height / ARENA_HEIGHT as f32 * window.height(), | |
1.0, | |
); | |
} | |
} | |
fn position_translation( | |
primary_window: Single<&Window, With<PrimaryWindow>>, | |
mut q: Query<(&Position, &mut Transform)>, | |
) { | |
fn convert(pos: f32, bound_window: f32, bound_game: f32) -> f32 { | |
let tile_size = bound_window / bound_game; | |
pos / bound_game * bound_window - (bound_window / 2.) + (tile_size / 2.) | |
} | |
let window = primary_window; | |
for (pos, mut transform) in q.iter_mut() { | |
transform.translation = Vec3::new( | |
convert(pos.x as f32, window.width(), ARENA_WIDTH as f32), | |
convert(pos.y as f32, window.height(), ARENA_HEIGHT as f32), | |
0.0, | |
); | |
} | |
} | |
fn game_over( | |
mut commands: Commands, | |
mut reader: EventReader<GameOverEvent>, | |
segments_res: ResMut<SnakeSegments>, | |
food: Query<Entity, With<Food>>, | |
segments: Query<Entity, With<SnakeSegment>>, | |
) { | |
if reader.read().next().is_some() { | |
for ent in food.iter().chain(segments.iter()) { | |
commands.entity(ent).despawn(); | |
} | |
spawn_snake(commands, segments_res); | |
} | |
} | |
fn close_on_esc( | |
mut commands: Commands, | |
focused_windows: Query<(Entity, &Window)>, | |
input: Res<ButtonInput<KeyCode>>, | |
) { | |
for (window, focus) in focused_windows.iter() { | |
if !focus.focused { | |
continue; | |
} | |
if input.just_pressed(KeyCode::Escape) { | |
commands.entity(window).despawn(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment