use std::default::Default; use bevy::core::FixedTimestep; use bevy::prelude::*; use bevy::render::pass::ClearColor; use rand::prelude::random; const ARENA_WIDTH: u32 = 10; const ARENA_HEIGHT: u32 = 10; struct SnakeHead { direction: Direction, } struct Food; struct GrowthEvent; #[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) -> Size { Self { width: x, height: x, } } } struct Materials { head_material: Handle, segment_material: Handle, food_material: Handle, } #[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, } } } #[derive(SystemLabel, Debug, Hash, PartialEq, Eq, Clone)] pub enum SnakeMovement { Input, Movement, Eating, Growth, } struct SnakeSegment; #[derive(Default)] struct SnakeSegments { segments: Vec, } fn main() { App::build() .insert_resource(WindowDescriptor { title: "Snake!".to_string(), width: 500.0, height: 500.0, ..Default::default() }) .insert_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04))) .insert_resource(SnakeSegments::default()) .add_startup_system(setup.system()) .add_startup_stage("game_setup", SystemStage::single(spawn_snake.system())) .add_plugins(DefaultPlugins) .add_system( snake_movement_input .system() .label(SnakeMovement::Input) .before(SnakeMovement::Movement), ) .add_system_set( SystemSet::new() .with_run_criteria(FixedTimestep::step(0.3)) .with_system(snake_movement.system().label(SnakeMovement::Movement)) .with_system( snake_eating .system() .label(SnakeMovement::Eating) .after(SnakeMovement::Movement), ), ) .add_system_set_to_stage( CoreStage::PostUpdate, SystemSet::new() .with_system(position_translation.system()) .with_system(size_scaling.system()), ) .add_system_set( SystemSet::new() .with_run_criteria(FixedTimestep::step(0.9)) .with_system(food_spawner.system()), ) .add_event::() .run(); } fn setup(mut commands: Commands, mut materials: ResMut>) { commands.spawn_bundle(OrthographicCameraBundle::new_2d()); 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 food_spawner(mut commands: Commands, materials: Res) { commands .spawn_bundle(SpriteBundle { material: materials.food_material.clone(), ..Default::default() }) .insert(Food) .insert(Position { x: (random::() * ARENA_WIDTH as f32) as i32, y: (random::() * ARENA_HEIGHT as f32) as i32, }) .insert(Size::square(0.8)); } fn spawn_segment( mut commands: Commands, material: &Handle, position: Position, ) -> Entity { commands .spawn_bundle(SpriteBundle { material: material.clone(), ..Default::default() }) .insert(SnakeSegment) .insert(position) .insert(Size::square(0.65)) .id() } fn spawn_snake( mut commands: Commands, materials: Res, mut segments: ResMut, ) { segments.segments = vec![ commands .spawn_bundle(SpriteBundle { material: materials.head_material.clone(), sprite: Sprite::new(Vec2::new(10.0, 10.0)), ..Default::default() }) .insert(SnakeHead { direction: Direction::Up, }) .insert(Position { x: 3, y: 3 }) .insert(Size::square(0.8)) .id(), spawn_segment( commands, &materials.segment_material, Position { x: 3, y: 2 }, ), ] } fn snake_movement_input(keyboard_input: Res>, mut heads: Query<&mut SnakeHead>) { if let Some(mut head) = heads.iter_mut().next() { let dir = 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; } } } fn snake_movement( segments: ResMut, mut heads: Query<(Entity, &SnakeHead)>, mut positions: Query<&mut Position>, ) { if let Some((head_entity, head)) = heads.iter_mut().next() { let segment_positions = segments .segments .iter() .map(|e| *positions.get_mut(*e).unwrap()) .collect::>(); 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, } segment_positions .iter() .zip(segments.segments.iter().skip(1)) .for_each(|(pos, segment)| { *positions.get_mut(*segment).unwrap() = *pos; }); } } fn snake_eating( mut commands: Commands, mut growth_writer: EventWriter, food_positions: Query<(Entity, &Position), With>, head_positions: Query<&Position, With>, ) { 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.send(GrowthEvent); } } } } fn size_scaling(windows: Res, mut q: Query<(&Size, &mut Sprite)>) { let window = windows.get_primary().unwrap(); for (size, mut sprite) in q.iter_mut() { sprite.size = Vec2::new( size.width / ARENA_WIDTH as f32 * window.width() as f32, 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.0) + (tile_size / 2.0) } let window = windows.get_primary().unwrap(); for (pos, mut tf) in q.iter_mut() { tf.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, ) } }