Skip to content

Instantly share code, notes, and snippets.

@blogscot
Created May 12, 2025 13:22
Show Gist options
  • Save blogscot/b00a301b043934f2da93537bdc4c0197 to your computer and use it in GitHub Desktop.
Save blogscot/b00a301b043934f2da93537bdc4c0197 to your computer and use it in GitHub Desktop.
Snake Game using Bevy v0.16.0
// 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