use bevy::prelude::*; use bevy::render::pass::ClearColor; use rand::prelude::random; use std::time::Duration; const ARENA_WIDTH: u32 = 10; const ARENA_HEIGHT: u32 = 10; #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] struct Position { x: i32, y: i32, } struct Size { width: f32, height: f32, } impl Size { pub fn square(x: f32) -> Self { Self { width: x, height: x, } } } struct SnakeHead { direction: Direction, } struct Materials { head_material: Handle, segment_material: Handle, food_material: Handle, } struct SnakeMoveTimer(Timer); struct GameOverEvent; struct GrowthEvent; #[derive(Default)] struct LastTailPosition(Option); struct SnakeSegment; #[derive(Default)] struct SnakeSegments(Vec); struct Food; struct FoodSpawnTimer(Timer); impl Default for FoodSpawnTimer { fn default() -> Self { Self(Timer::new(Duration::from_millis(1000), true)) } } #[derive(PartialEq, Copy, Clone)] 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 setup(commands: &mut Commands, mut materials: ResMut>) { commands.spawn(Camera2dBundle::default()); commands.insert_resource(Materials { head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()), segment_material: materials.add(Color::rgb(0.3, 0.3, 0.3).into()), food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()), }); } fn spawn_snake( commands: &mut Commands, materials: Res, mut segments: ResMut, ) { segments.0 = vec![ commands .spawn(SpriteBundle { material: materials.head_material.clone(), sprite: Sprite::new(Vec2::new(10.0, 10.0)), ..Default::default() }) .with(SnakeHead { direction: Direction::Up, }) .with(SnakeSegment) .with(Position { x: 3, y: 3 }) .with(Size::square(0.8)) .current_entity() .unwrap(), spawn_segment( commands, &materials.segment_material, Position { x: 3, y: 2 }, ), ]; } fn spawn_segment( commands: &mut Commands, material: &Handle, position: Position, ) -> Entity { commands .spawn(SpriteBundle { material: material.clone(), ..Default::default() }) .with(SnakeSegment) .with(position) .with(Size::square(0.65)) .current_entity() .unwrap() } fn snake_movement( keyboard_input: Res>, snake_timer: ResMut, mut game_over_events: ResMut>, mut last_tail_position: ResMut, segments: ResMut, mut heads: Query<(Entity, &mut SnakeHead)>, mut positions: Query<&mut Position>, ) { if let Some((head_entity, mut head)) = heads.iter_mut().next() { let segment_positions = segments .0 .iter() .map(|e| *positions.get_mut(*e).unwrap()) .collect::>(); let mut head_pos = positions.get_mut(head_entity).unwrap(); let dir: Direction = if keyboard_input.pressed(KeyCode::Left) { Direction::Left } else if keyboard_input.pressed(KeyCode::Down) { Direction::Down } else if keyboard_input.pressed(KeyCode::Up) { Direction::Up } else if keyboard_input.pressed(KeyCode::Right) { Direction::Right } else { head.direction }; if dir != head.direction.opposite() { head.direction = dir; } if !snake_timer.0.finished() { return; } 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; } }; // Check if we've hit a wall 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_events.send(GameOverEvent); } // Check if we've hit our tail if segment_positions.contains(&head_pos) { game_over_events.send(GameOverEvent); } segment_positions .iter() .zip(segments.0.iter().skip(1)) .for_each(|(pos, segment)| { *positions.get_mut(*segment).unwrap() = *pos; }); last_tail_position.0 = Some(*segment_positions.last().unwrap()); } } fn game_over( commands: &mut Commands, mut reader: Local>, game_over_events: Res>, materials: Res, segments_res: ResMut, food: Query>, segments: Query>, ) { if reader.iter(&game_over_events).next().is_some() { for ent in food.iter().chain(segments.iter()) { commands.despawn(ent); } spawn_snake(commands, materials, segments_res); } } fn snake_eating( commands: &mut Commands, snake_timer: ResMut, mut growth_events: ResMut>, food_positions: Query<(Entity, &Position), With>, head_positions: Query<&Position, With>, ) { if !snake_timer.0.finished() { return; } for head_pos in head_positions.iter() { for (ent, food_pos) in food_positions.iter() { if food_pos == head_pos { commands.despawn(ent); growth_events.send(GrowthEvent); } } } } fn snake_growth( commands: &mut Commands, last_tail_position: Res, growth_events: Res>, mut segments: ResMut, mut growth_reader: Local>, materials: Res, ) { if growth_reader.iter(&growth_events).next().is_some() { segments.0.push(spawn_segment( commands, &materials.segment_material, last_tail_position.0.unwrap(), )); } } fn size_scaling(windows: Res, 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 / ARENA_WIDTH as f32 * window.width() as f32, sprite_size.height / ARENA_HEIGHT as f32 * window.height() as f32, ); } } fn position_translation(windows: Res, 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 = 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, ARENA_WIDTH as f32), convert(pos.y as f32, window.height() as f32, ARENA_HEIGHT as f32), 0.0, ); } } fn food_spawner( commands: &mut Commands, materials: Res, time: Res