From 4c5a1d127053e9452decdce3118da6e2fd5c71f2 Mon Sep 17 00:00:00 2001 From: Alexander Baranov Date: Wed, 3 Jun 2026 19:46:37 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BD?= =?UTF-8?q?=D0=B0=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20-=20WIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snake-game/src/debug.rs | 55 ++++++++++++++++++++++++ snake-game/src/domain.rs | 10 ++--- snake-game/src/gameplay.rs | 9 +++- snake-game/src/lib.rs | 44 ++----------------- snake-game/src/main.rs | 6 ++- snake-game/src/observers.rs | 55 ++++++++++++++++++++++++ snake-game/src/startup.rs | 46 +++----------------- snake-game/src/tools.rs | 85 ++++++++++++++++--------------------- 8 files changed, 171 insertions(+), 139 deletions(-) create mode 100644 snake-game/src/debug.rs create mode 100644 snake-game/src/observers.rs diff --git a/snake-game/src/debug.rs b/snake-game/src/debug.rs new file mode 100644 index 0000000..a70919b --- /dev/null +++ b/snake-game/src/debug.rs @@ -0,0 +1,55 @@ +use bevy::prelude::*; + +/// ДЛЯ ОТЛАДКИ - вывод всех сущностей мира +pub fn print_all_entities(mut commands: Commands, query: Query) { + for entity in query { + commands.entity(entity).log_components(); + } +} + +/// ДЛЯ ОТЛАДКИ - вывод всех сущностей мира по нажатию Enter +pub fn print_all_entities_by_press_enter( + input: Res>, + mut commands: Commands, + query: Query, +) { + if input.just_pressed(KeyCode::Enter) { + for entity in query { + commands.entity(entity).log_components(); + } + } +} + +/// Координатная сетка +#[derive(Component)] +pub struct Grid; + +/// Создать координатную сетку +pub fn spawn_grid(commands: &mut Commands) { + use crate::domain::GRID_SIZE; + + let grid = commands + .spawn((Grid, Transform::default(), Visibility::default())) + .id(); + + for x in -20..=20 { + commands.spawn(( + Sprite::from_color(Color::srgb(1.0, 1.0, 0.5), vec2(0.5, 2000.0)), + Transform { + translation: vec3(x as f32 * GRID_SIZE as f32, 0.0, -1.0), + ..Default::default() + }, + ChildOf(grid), + )); + } + for y in -20..=20 { + commands.spawn(( + Sprite::from_color(Color::srgb(1.0, 1.0, 0.5), vec2(2000.0, 0.5)), + Transform { + translation: vec3(0.0, y as f32 * GRID_SIZE as f32, -1.0), + ..Default::default() + }, + ChildOf(grid), + )); + } +} diff --git a/snake-game/src/domain.rs b/snake-game/src/domain.rs index 788ff6c..3a18128 100644 --- a/snake-game/src/domain.rs +++ b/snake-game/src/domain.rs @@ -7,10 +7,6 @@ pub const GRID_SIZE: u8 = 48; pub const HEAD_SIZE: u8 = (GRID_SIZE as f32 * 1.1) as u8; pub const SNAKE_VELOCITY: u8 = (GRID_SIZE as f32 / 1.0) as u8; -/// Координатная сетка -#[derive(Component)] -pub struct Grid; - /// Голова змеи #[derive(Component)] pub struct Head; @@ -118,9 +114,9 @@ impl SnakePath { self.0.get(&key).cloned() } - pub fn pop(&mut self, transform: &Transform) -> Option { + pub fn drop(&mut self, transform: &Transform) -> Option<(i16, i16, Direction)> { let key = SnakePath::to_key(transform); - self.0.remove(&key) + self.0.remove(&key).map(|d| (key.0, key.1, d)) } } @@ -139,7 +135,7 @@ mod test { assert!(snake_path.pick(&Transform::default()) == Some(Direction::default())); assert_eq!(snake_path.0.len(), 1); - assert!(snake_path.pop(&Transform::default()) == Some(Direction::default())); + assert!(snake_path.drop(&Transform::default()) == Some((0, 0, Direction::default()))); assert_eq!(snake_path.0.len(), 0); } } diff --git a/snake-game/src/gameplay.rs b/snake-game/src/gameplay.rs index 75ad1c3..3be1a1e 100644 --- a/snake-game/src/gameplay.rs +++ b/snake-game/src/gameplay.rs @@ -15,15 +15,20 @@ pub fn head_rotation( let (mut transform, mut direction) = query.into_inner(); + let mut append_snake_path = |transform: &Transform, direction: &Direction| { + snake_path.push(&transform, direction.clone()); + trace!("append snake_path: {:?}", snake_path); + }; + if input.just_pressed(KeyCode::ArrowLeft) { transform.rotate_z(PI / 2.0); direction.rotate_left(); - snake_path.push(&transform, direction.clone()); + append_snake_path(&transform, &direction); } if input.just_pressed(KeyCode::ArrowRight) { transform.rotate_z(-PI / 2.0); direction.rotate_right(); - snake_path.push(&transform, direction.clone()); + append_snake_path(&transform, &direction); } } diff --git a/snake-game/src/lib.rs b/snake-game/src/lib.rs index 22a2e3e..ed2bc21 100644 --- a/snake-game/src/lib.rs +++ b/snake-game/src/lib.rs @@ -1,46 +1,10 @@ -/// Инициализация логирования -pub fn define_log_layer() -> Option { - use std::fs::File; - use tracing::Level; - use tracing_subscriber::{Layer, filter::Targets, fmt::layer}; - - let targets = Targets::new().with_default(Level::TRACE); - - let stdout = layer() - .compact() - .with_thread_names(true) - .with_file(false) - .with_line_number(false) - .with_filter(targets.clone()) - .boxed(); - let file = match File::options() - .write(true) - .create(true) - .append(true) - .open("snake-game.log") - { - Ok(file) => file, - Err(e) => { - eprintln!("No access to log file: {:?}", e); - return Some(stdout); - } - }; - let log_file = layer() - .compact() - .with_ansi(false) - .with_thread_names(true) - .with_file(false) - .with_line_number(false) - .with_writer(file) - .with_filter(targets.clone()) - .boxed(); - Some(Box::new(vec![stdout, log_file])) -} - +pub mod debug; mod domain; mod gameplay; +mod observers; mod startup; mod tools; pub use gameplay::{head_rotation, moving}; -pub use startup::{print_all_entities, setup_frame_rate, startup}; +pub use startup::{setup_frame_rate, startup}; +pub use tools::define_log_layer; diff --git a/snake-game/src/main.rs b/snake-game/src/main.rs index 12e4eea..617587d 100644 --- a/snake-game/src/main.rs +++ b/snake-game/src/main.rs @@ -19,7 +19,11 @@ fn main() { .insert_resource(bevy::winit::WinitSettings::desktop_app()) .insert_resource(ClearColor(Color::srgb(0.3, 0.6, 0.8))) .add_systems(Startup, (startup, setup_frame_rate)) - // .add_systems(PostStartup, snake_game::print_all_entities) + // .add_systems(PostStartup, snake_game::debug::print_all_entities) .add_systems(Update, (head_rotation, moving)) + // .add_systems( + // PostUpdate, + // snake_game::debug::print_all_entities_by_press_enter, + // ) .run(); } diff --git a/snake-game/src/observers.rs b/snake-game/src/observers.rs new file mode 100644 index 0000000..21f75bf --- /dev/null +++ b/snake-game/src/observers.rs @@ -0,0 +1,55 @@ +use crate::domain::*; + +use bevy::prelude::*; + +#[allow(unused)] +/// Слушатель событий [GridBarier] для хвоста, обновляющий направление его движения из ресурса [SnakePath] и позволяющий следовать за головой +pub fn tail_observer( + event: On, + mut query: Query<(&mut Direction, &Transform)>, + snake_path: Res, +) { + trace!("...changing direction: triggered"); + if event.observer() == event.0 { + trace!("...changing direction: observer == event.0"); + if let Ok((mut direction, transform)) = query.get_mut(event.0) { + trace!("...changing direction: query success"); + if let Some(new_direction) = snake_path.pick(transform) { + trace!("change direction for {:?} to {:?}", event.0, new_direction); + *direction = new_direction; + } + } + } +} + +/// Слушатель событий [GridBarier] для конца змеи, обновляющий направление его движения из ресурса [SnakePath] и удаляющий полученные значения из [SnakePath] +pub fn cleanup_snake_path( + event: On, + mut commands: Commands, + query: Query<&Transform, With>, +) { + if let Ok(transform) = query.get(event.0) { + // trace!("schedule cleaning SnakePath for {:?}", transform); + let transform = transform.clone(); + commands.queue(move |world: &mut World| { + let mut snake_path = world.resource_mut::(); + if let Some((x, y, dir)) = snake_path.drop(&transform) { + trace!( + "removed entry from SnakePath: ({:?}, {:?}) => {:?}\n\tsnake_path: {:?}", + x, y, dir, *snake_path + ); + } + }); + } +} + +/// Запланировать установку скорости движения [Velocity] при следующем событии [GridBarier] +pub fn schedule_set_velocity(tail: Entity) -> impl Fn(On, Commands) { + move |event: On, mut commands: Commands| { + commands + .entity(tail) + .entry::() + .and_modify(|mut velocity| velocity.0 = SNAKE_VELOCITY as f32); + commands.entity(event.observer()).despawn(); + } +} diff --git a/snake-game/src/startup.rs b/snake-game/src/startup.rs index 6b42cfd..95fea64 100644 --- a/snake-game/src/startup.rs +++ b/snake-game/src/startup.rs @@ -2,8 +2,8 @@ use bevy::{prelude::*, winit::WinitSettings}; use std::time::Duration; use crate::{ - domain::{End, Grid, SnakePath, Tail}, - tools::{end_observer, schedule_set_velocity}, + domain::{End, SnakePath, Tail}, + observers::{cleanup_snake_path, schedule_set_velocity, tail_observer}, }; /// Настройка частоты кадров @@ -19,7 +19,7 @@ pub fn setup_frame_rate(mut winit: ResMut) { /// Инициализация игрового мира pub fn startup(mut commands: Commands) { spawn_snake(&mut commands); - spawn_grid(&mut commands); + crate::debug::spawn_grid(&mut commands); commands.spawn(Camera2d); } @@ -28,6 +28,7 @@ fn spawn_snake(commands: &mut Commands) { use crate::domain::{Direction, HEAD_SIZE, Head, Inert, SNAKE_VELOCITY, Velocity}; commands.insert_resource(SnakePath::default()); + commands.spawn(Observer::new(cleanup_snake_path)); let head = commands .spawn(( @@ -83,45 +84,8 @@ fn spawn_snake(commands: &mut Commands) { translation: vec3(0.0, 0.0, 0.0), ..Default::default() }, + Observer::new(tail_observer), )) .id(); commands.entity(head).observe(schedule_set_velocity(tail)); - commands.entity(tail).observe(end_observer); -} - -/// Создать координатную сетку -fn spawn_grid(commands: &mut Commands) { - use crate::domain::GRID_SIZE; - - let grid = commands - .spawn((Grid, Transform::default(), Visibility::default())) - .id(); - - for x in -20..=20 { - commands.spawn(( - Sprite::from_color(Color::srgb(1.0, 1.0, 0.5), vec2(0.5, 2000.0)), - Transform { - translation: vec3(x as f32 * GRID_SIZE as f32, 0.0, -1.0), - ..Default::default() - }, - ChildOf(grid), - )); - } - for y in -20..=20 { - commands.spawn(( - Sprite::from_color(Color::srgb(1.0, 1.0, 0.5), vec2(2000.0, 0.5)), - Transform { - translation: vec3(0.0, y as f32 * GRID_SIZE as f32, -1.0), - ..Default::default() - }, - ChildOf(grid), - )); - } -} - -/// ДЛЯ ОТЛАДКИ - вывод всех сущностей мира -pub fn print_all_entities(mut commands: Commands, query: Query) { - for entity in query { - commands.entity(entity).log_components(); - } } diff --git a/snake-game/src/tools.rs b/snake-game/src/tools.rs index 76ed419..baf600d 100644 --- a/snake-game/src/tools.rs +++ b/snake-game/src/tools.rs @@ -1,6 +1,41 @@ -use bevy::{ecs::observer::ObservedBy, prelude::*}; +/// Инициализация логирования +pub fn define_log_layer() -> Option { + use std::fs::File; + use tracing::Level; + use tracing_subscriber::{Layer, filter::Targets, fmt::layer}; -use crate::domain::*; + let targets = Targets::new().with_default(Level::TRACE); + + let stdout = layer() + .compact() + .with_thread_names(true) + .with_file(false) + .with_line_number(false) + .with_filter(targets.clone()) + .boxed(); + let file = match File::options() + .write(true) + .create(true) + .append(true) + .open("snake-game.log") + { + Ok(file) => file, + Err(e) => { + eprintln!("No access to log file: {:?}", e); + return Some(stdout); + } + }; + let log_file = layer() + .compact() + .with_ansi(false) + .with_thread_names(true) + .with_file(false) + .with_line_number(false) + .with_writer(file) + .with_filter(targets.clone()) + .boxed(); + Some(Box::new(vec![stdout, log_file])) +} /// Разбиение изменения координаты по границам сетки pub fn split_by_grid(src: f32, dst: f32, grid: f32) -> Vec<(f32, f32)> { @@ -40,52 +75,6 @@ pub fn split_by_grid(src: f32, dst: f32, grid: f32) -> Vec<(f32, f32)> { output } -#[allow(unused)] -/// Слушатель событий [GridBarier] для хвоста, обновляющий направление его движения из ресурса [SnakePath] -pub fn tail_observer( - event: On, - mut query: Query<(&mut Direction, &Transform), Without>, - snake_path: Res, -) { - if let Ok((mut direction, transform)) = query.get_mut(event.0) { - if let Some(new_direction) = snake_path.pick(transform) { - trace!("change direction for {:?} to {:?}", event.0, new_direction); - *direction = new_direction; - } - } -} - -/// Слушатель событий [GridBarier] для конца змеи, обновляющий направление его движения из ресурса [SnakePath] и удаляющий полученные значения из [SnakePath] -pub fn end_observer( - event: On, - mut query: Query<(&mut Direction, &Transform), With>, - mut snake_path: ResMut, -) { - if let Ok((mut direction, transform)) = query.get_mut(event.0) { - if let Some(new_direction) = snake_path.pop(transform) { - trace!( - "change direction for {:?} to {:?} and remove it from SnakePath", - event.0, new_direction - ); - *direction = new_direction; - } - } -} - -/// Запланировать установку скорости движения [Velocity] при следующем событии [GridBarier] -pub fn schedule_set_velocity( - tail: Entity, -) -> impl Fn(On, Commands, Query<&mut Velocity, With>) { - move |event: On, - mut commands: Commands, - mut query: Query<&mut Velocity, With>| { - if let Ok(mut velocity) = query.get_mut(tail) { - velocity.0 = SNAKE_VELOCITY as f32; - } - commands.entity(event.0).remove::(); - } -} - #[cfg(test)] mod test { use crate::tools::*;