diff --git a/snake-game/Cargo.toml b/snake-game/Cargo.toml index 24cf1da..4f77971 100644 --- a/snake-game/Cargo.toml +++ b/snake-game/Cargo.toml @@ -8,9 +8,9 @@ bevy = { version = "0.18", default-features = false, features = ["2d_bevy_render # bevy = { version = "0.18" } tracing = "0.1" tracing-subscriber = "0.3" +rand = "0.10" # for examples -rand = "0.10" rand_chacha = "0.10" [profile.release] diff --git a/snake-game/src/debug.rs b/snake-game/src/debug.rs index 958aa43..ae49e24 100644 --- a/snake-game/src/debug.rs +++ b/snake-game/src/debug.rs @@ -22,9 +22,8 @@ pub fn print_all_entities_by_press_enter(input: Res>, mut c } /// Замедлить время и поставить на паузу -pub fn set_initial_time_speed_to_slow_and_pause(mut time: ResMut>) { +pub fn set_initial_time_speed_to_slow(mut time: ResMut>) { time.set_relative_speed(1.0 / 8.0); - time.pause(); } /// обертка над функцией [crate::tools::split_by_grid] для ее отладки diff --git a/snake-game/src/domain.rs b/snake-game/src/domain.rs index 42ae89a..5fd6b23 100644 --- a/snake-game/src/domain.rs +++ b/snake-game/src/domain.rs @@ -14,8 +14,10 @@ pub use position::Position; pub const GRID_SIZE: u8 = 32; pub const HEAD_SIZE: u8 = (GRID_SIZE as f32 * 1.1).ceil() as u8; -pub const SNAKE_VELOCITY: u8 = (GRID_SIZE as f32 / 0.33).ceil() as u8; +pub const SNAKE_VELOCITY: u8 = (GRID_SIZE as f32 * 3.0).ceil() as u8; pub const FIELD_SIZE: u8 = 20; +pub const MAX_MEALS_ON_FIELD: u8 = 5; +pub const MEAL_SIZE: u8 = (GRID_SIZE as f32 * 0.75).ceil() as u8; /// Голова змеи #[derive(Component)] @@ -36,3 +38,13 @@ pub struct Velocity(pub f32); /// Еда #[derive(Component)] pub struct Meal; + +/// Таймер респавна еды +#[derive(Resource, Deref, DerefMut)] +pub struct MealRespawnTimer(Timer); + +impl MealRespawnTimer { + pub fn new(period: f32) -> Self { + Self(Timer::from_seconds(period, TimerMode::Repeating)) + } +} diff --git a/snake-game/src/domain/direction.rs b/snake-game/src/domain/direction.rs index 6efb60e..eb44749 100644 --- a/snake-game/src/domain/direction.rs +++ b/snake-game/src/domain/direction.rs @@ -1,8 +1,9 @@ use bevy::prelude::*; /// Направление движения -#[derive(Component, PartialEq, Clone, Copy, Debug)] +#[derive(Component, PartialEq, Clone, Copy, Debug, Default)] pub enum Direction { + #[default] Up, Left, Down, @@ -67,12 +68,6 @@ impl Direction { } } -impl Default for Direction { - fn default() -> Self { - Direction::Up - } -} - impl std::fmt::Display for Direction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/snake-game/src/fixed_update.rs b/snake-game/src/fixed_update.rs index 7ade808..fda4d6a 100644 --- a/snake-game/src/fixed_update.rs +++ b/snake-game/src/fixed_update.rs @@ -1,4 +1,7 @@ -use bevy::prelude::*; +use std::collections::HashSet; + +use bevy::{color::palettes::tailwind::*, prelude::*}; +use rand::RngExt; use crate::{domain::*, tools::split_by_grid}; @@ -7,19 +10,19 @@ fn moving( velocity: &f32, time: &Time, direction: &mut Direction, - mut transform: &mut Transform, + transform: &mut Transform, entity: Entity, commands: &mut Commands, switch_direction: impl FnOnce(Position) -> Option, ) { // первая часть движения - до границы сетки let delta = velocity * time.delta_secs(); - let src = direction.get_coord(&transform); + let src = direction.get_coord(transform); let sign = direction.get_change_sign(); let dst = src + sign * delta; let mut splited = split_by_grid(src, dst, GRID_SIZE as f32).into_iter(); let first = splited.next().unwrap_or((0.0, 0.0)); - direction.set_coord(&mut transform, first.0); + direction.set_coord(transform, first.0); // вторая часть движения - после границы сетки if let Some(second) = splited.next() { @@ -32,10 +35,10 @@ fn moving( let src = direction.get_coord(&*transform); let sign = direction.get_change_sign(); let dst = src + sign * delta; - direction.set_coord(&mut transform, dst); + direction.set_coord(transform, dst); // создать событие прохождения границы сетки - commands.entity(entity).trigger(|e| GridBarier::new(e, pos, (&*direction).clone())); + commands.entity(entity).trigger(|e| GridBarier::new(e, pos, *direction)); } } @@ -48,12 +51,12 @@ pub fn head_moving( ) { let _span = trace_span!("head_moving").entered(); for (mut transform, mut direction, Velocity(velocity), head, entity) in query.into_iter() { - let next_dir = head.0.clone(); - let dir = direction.clone(); + let next_dir = head.0; + let dir = *direction; let snake_path = &mut snake_path; let switch_direction = move |pos: Position| { if next_dir != dir { - snake_path.insert(pos.clone(), next_dir.clone()); + snake_path.insert(pos, next_dir); Some(next_dir) } else { None @@ -63,6 +66,7 @@ pub fn head_moving( } } +#[allow(clippy::type_complexity)] /// Система движения тела змеи pub fn snake_moving( query: Query<(&mut Transform, &mut Direction, &Velocity, Entity), (With, Without)>, @@ -77,3 +81,34 @@ pub fn snake_moving( moving(velocity, &time, &mut direction, &mut transform, entity, &mut commands, switch_direction); } } + +/// Система спавна еды при необходимости +pub fn spawn_meal_if_needed( + time: Res>, // + mut meal_timer: ResMut, + query: Query<&Transform, With>, + mut commands: Commands, +) { + if meal_timer.tick(time.elapsed()).just_finished() && query.count() < MAX_MEALS_ON_FIELD as usize { + let used_positions = query.iter().map(Position::from).collect::>(); + let mut rng = rand::rng(); + let pos = loop { + const ABS: i8 = (FIELD_SIZE as f32 / 2.0) as i8; + let rand_x = rng.random::() % ABS; + let rand_y = rng.random::() % ABS; + let rand_pos = Position::new(rand_x, rand_y); + if !used_positions.contains(&rand_pos) { + break rand_pos; + } + }; + commands.spawn(( + Meal, + Sprite::from_color(YELLOW_400, vec2(MEAL_SIZE as f32, MEAL_SIZE as f32)), // + Transform { + translation: pos.translation().extend(0.0), + ..Default::default() + }, + )); + trace!("new meal spawned at {pos}"); + } +} diff --git a/snake-game/src/game.rs b/snake-game/src/game.rs index 0e81fb6..fba6837 100644 --- a/snake-game/src/game.rs +++ b/snake-game/src/game.rs @@ -20,6 +20,7 @@ impl Plugin for SnakeGame { app.add_systems(Startup, crate::startup::setup_window); app.add_systems(Startup, crate::startup::setup_time); + app.add_systems(Startup, crate::startup::pause_on_startup); app.add_systems(Startup, crate::startup::startup); app.add_systems(Update, crate::update::head_rotation); @@ -31,12 +32,13 @@ impl Plugin for SnakeGame { app.add_systems(FixedUpdate, crate::fixed_update::head_moving); app.add_systems(FixedUpdate, crate::fixed_update::snake_moving); + app.add_systems(FixedUpdate, crate::fixed_update::spawn_meal_if_needed); // DEBUG: - app.add_systems(Startup, crate::debug::set_initial_time_speed_to_slow_and_pause); + // app.add_systems(Startup, crate::debug::set_initial_time_speed_to_slow); // app.add_systems(PostStartup, crate::debug::print_all_entities); - app.add_systems(Update, crate::debug::grow_up_on_tab); + // app.add_systems(Update, crate::debug::grow_up_on_tab); // app.add_systems(Update, crate::debug::print_all_entities_by_press_enter); } } diff --git a/snake-game/src/observers.rs b/snake-game/src/observers.rs index b27bacf..1dda38d 100644 --- a/snake-game/src/observers.rs +++ b/snake-game/src/observers.rs @@ -24,6 +24,7 @@ pub fn clean_snake_path(event: On, query: Query<&End>, mut snake_pat } /// Обработка события [GrowUp] +#[allow(clippy::type_complexity)] pub fn grow_up_snake(_: On, mut commands: Commands, query: Single<(Entity, &Direction, &Transform, &mut Sprite), (With, With)>) { let (entity, direction, transform, mut sprite) = query.into_inner(); sprite.color = GRAY_300.into(); @@ -33,7 +34,7 @@ pub fn grow_up_snake(_: On, mut commands: Commands, query: Single<(Entit .spawn(( Tail(entity), End, - direction.clone(), + *direction, Velocity(0.0), Sprite::from_color(YELLOW_500, vec2(HEAD_SIZE as f32 * 0.8, HEAD_SIZE as f32 * 0.8)), Transform { @@ -46,6 +47,7 @@ pub fn grow_up_snake(_: On, mut commands: Commands, query: Single<(Entit } /// Обработка столкновений +#[allow(clippy::type_complexity)] pub fn handle_collisions( event: On, // query: Query<(&mut Velocity, &mut Sprite), Or<(With, With, With)>>, diff --git a/snake-game/src/post_update.rs b/snake-game/src/post_update.rs index d56c4e2..7084e6c 100644 --- a/snake-game/src/post_update.rs +++ b/snake-game/src/post_update.rs @@ -10,7 +10,7 @@ pub fn check_bounds_colisions( let (head_transform,) = head.into_inner(); let (x, y) = (head_transform.translation.x, head_transform.translation.y); const SIDE: f32 = (FIELD_SIZE as f32 / 2.0) * GRID_SIZE as f32; - if x < -SIDE || x > SIDE || y < -SIDE || y > SIDE { + if !(-SIDE..=SIDE).contains(&x) || !(-SIDE..=SIDE).contains(&y) { commands.trigger(Collision::Bounds); }; } diff --git a/snake-game/src/startup.rs b/snake-game/src/startup.rs index e519891..b2187ce 100644 --- a/snake-game/src/startup.rs +++ b/snake-game/src/startup.rs @@ -1,6 +1,6 @@ use bevy::{color::palettes::tailwind::*, prelude::*}; -use crate::domain::{FIELD_SIZE, GRID_SIZE, SnakePath}; +use crate::domain::{FIELD_SIZE, GRID_SIZE, MealRespawnTimer, SnakePath}; /// Настройка игрового окна pub fn setup_window(mut window: Single<&mut Window>) { @@ -15,10 +15,16 @@ pub fn setup_time(mut time: ResMut>) { time.set_timestep_hz(32.0); } +/// Начать с паузы +pub fn pause_on_startup(mut time: ResMut>) { + time.pause(); +} + /// Инициализация игрового мира pub fn startup(mut commands: Commands) { commands.insert_resource(ClearColor(SKY_600.into())); commands.insert_resource(SnakePath::default()); + commands.insert_resource(MealRespawnTimer::new(2.0)); commands.spawn(Observer::new(crate::observers::clean_snake_path)); commands.spawn(Observer::new(crate::observers::grow_up_snake)); @@ -34,6 +40,6 @@ pub fn startup(mut commands: Commands) { // commands.spawn(Observer::new(crate::debug::trace_collisions)); // commands.spawn(Observer::new(crate::debug::trace_snake_moving)); - crate::debug::spawn_grid(&mut commands); - crate::debug::spawn_some_meal(&mut commands); + // crate::debug::spawn_grid(&mut commands); + // crate::debug::spawn_some_meal(&mut commands); } diff --git a/snake-game/src/tools.rs b/snake-game/src/tools.rs index bc1609c..f9efa92 100644 --- a/snake-game/src/tools.rs +++ b/snake-game/src/tools.rs @@ -13,7 +13,7 @@ pub fn define_log_layer() -> Option { .with_line_number(false) .with_filter(targets.clone()) .boxed(); - let file = match File::options().write(true).create(true).append(true).open("snake-game.log") { + let file = match File::options().create(true).append(true).open("snake-game.log") { Ok(file) => file, Err(e) => { eprintln!("No access to log file: {:?}", e); diff --git a/snake-game/src/update.rs b/snake-game/src/update.rs index 957455f..72423e9 100644 --- a/snake-game/src/update.rs +++ b/snake-game/src/update.rs @@ -9,19 +9,19 @@ pub fn head_rotation( ) { if input.just_pressed(KeyCode::ArrowLeft) { let (mut transform, mut head, direction) = query.into_inner(); - let mut new_direction = direction.clone(); + let mut new_direction = *direction; new_direction.rotate_left(); head.0 = new_direction; transform.rotation = new_direction.orientation(); } else if input.just_pressed(KeyCode::ArrowRight) { let (mut transform, mut head, direction) = query.into_inner(); - let mut new_direction = direction.clone(); + let mut new_direction = *direction; new_direction.rotate_right(); head.0 = new_direction; transform.rotation = new_direction.orientation(); } else if input.just_pressed(KeyCode::ArrowUp) { let (mut transform, mut head, direction) = query.into_inner(); - head.0 = direction.clone(); + head.0 = *direction; transform.rotation = direction.orientation(); } }