Compare commits

..

7 commits

Author SHA1 Message Date
nquidox
b95dde79a1 rename 2026-04-17 17:22:30 +03:00
nquidox
393e199a00 linear state restart action 2026-04-17 17:22:23 +03:00
nquidox
6506955115 move round balls system + extended spawn system 2026-04-17 16:26:43 +03:00
nquidox
0c7ceb5d8d draw arc by lines instead of arc_2d 2026-04-17 16:25:26 +03:00
nquidox
a1180ea769 additional track fields + simplified arc helper 2026-04-17 16:24:53 +03:00
nquidox
5ba555b062 spawn ball + run in set 2026-04-16 23:05:31 +03:00
nquidox
7b334b33c0 constants moved 2026-04-16 22:34:54 +03:00
13 changed files with 385 additions and 86 deletions

View file

@ -13,7 +13,7 @@ mod ui;
use crate::states::game::state::MainGameState; use crate::states::game::state::MainGameState;
use crate::states::main_menu::state::MainMenuState; use crate::states::main_menu::state::MainMenuState;
use crate::states::settings_menu::state::SettingsMenuState; use crate::states::settings_menu::state::SettingsMenuState;
use crate::states::linear::plugin::LinearPlugin; use crate::states::linear::plugin::LinearPlayPlugin;
const FACTOR: u32 = 80; const FACTOR: u32 = 80;
const WIDTH: u32 = 16; const WIDTH: u32 = 16;
@ -25,7 +25,7 @@ fn main() {
.add_message::<ExitRequestMessage>() .add_message::<ExitRequestMessage>()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window { primary_window: Some(Window {
title: "alpha".into(), title: "alpha v0.2".into(),
resolution: (WIDTH * FACTOR, HEIGHT * FACTOR).into(), resolution: (WIDTH * FACTOR, HEIGHT * FACTOR).into(),
resizable: true, resizable: true,
..default() ..default()
@ -37,7 +37,7 @@ fn main() {
MainMenuState, MainMenuState,
SettingsMenuState, SettingsMenuState,
MainGameState, MainGameState,
LinearPlugin, LinearPlayPlugin,
)) ))
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, exit_system) .add_systems(Update, exit_system)

View file

@ -7,6 +7,6 @@ pub enum AppState {
SettingsMenu, SettingsMenu,
GameState, GameState,
GameRestart, GameRestart,
LevelState, LinearPlayState,
LinearState, LinearGameRestart,
} }

View file

@ -1,4 +1,7 @@
use bevy::prelude::*; use bevy::prelude::*;
#[derive(Component)] #[derive(Component)]
pub struct LinearStateMarker; pub struct LinearStateMarker;
#[derive(Component, Copy, Clone)]
pub struct LinearRestartMarker;

View file

@ -0,0 +1,35 @@
use bevy::prelude::Component;
use rand::prelude::IndexedRandom;
#[derive(Component, Debug, Clone, Copy)]
pub struct RoundBall {
pub ball_type: RoundBallType,
pub track_progress: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum RoundBallType {
#[default]
Red,
Green,
Blue,
Purple,
}
impl RoundBallType {
pub const fn asset_path(&self) -> &'static str {
match self {
RoundBallType::Red => "sprites/round/red.png",
RoundBallType::Green => "sprites/round/green.png",
RoundBallType::Blue => "sprites/round/blue.png",
RoundBallType::Purple => "sprites/round/purple.png",
}
}
pub fn random_from_4() -> Self {
[Self::Red, Self::Green, Self::Blue, Self::Purple]
.choose(&mut rand::rng())
.copied()
.unwrap_or_default()
}
}

View file

@ -27,7 +27,10 @@ pub enum PathSegment {
#[derive(Resource)] #[derive(Resource)]
pub struct PrecalculatedTrack{ pub struct PrecalculatedTrack{
pub segments: Vec<PTSegment> pub segments: Vec<PTSegment>,
pub min_spawn_gap: f32, //нормализован
pub total_length: f32,
pub speed_norm: f32,
} }
#[derive(Debug)] #[derive(Debug)]
@ -41,6 +44,7 @@ pub struct PTSegment {
pub radius: f32, pub radius: f32,
pub start_angle: f32, pub start_angle: f32,
pub sweep_sign: f32, pub sweep_sign: f32,
pub length: f32,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]

View file

@ -0,0 +1,18 @@
use crate::FACTOR;
use bevy::color::Color;
pub const CENTER_X: f32 = 0.0;
pub const CENTER_Y: f32 = 0.0;
// pub const FACTOR_RADIUS: f32 = FACTOR as f32 / 2.0;
pub const STEP: f32 = FACTOR as f32 / SCALE;
pub const SCALE: f32 = 2.0;
// Z-INDEXES
pub const ROUND_BALL_Z: f32 = 10.0;
// COLORS
pub const PINK: Color = Color::srgb_u8(250, 0, 155);
pub const BLUE: Color = Color::srgb_u8(0, 0, 255);
pub const GREEN: Color = Color::srgb_u8(0, 255, 0);

View file

@ -11,3 +11,15 @@ pub use components_track::*;
mod systems_track; mod systems_track;
pub use systems_track::*; pub use systems_track::*;
mod components_ball;
pub use components_ball::*;
mod system_ball;
pub use system_ball::*;
mod constants;
pub use constants::*;
mod ui;
pub use ui::*;

View file

@ -1,23 +1,43 @@
use bevy::prelude::*; use crate::states::AppState;
use crate::states::AppState::LinearState; use crate::states::AppState::{LinearGameRestart, LinearPlayState};
use crate::states::level::GameOver;
use crate::states::linear::*; use crate::states::linear::*;
use bevy::prelude::*;
pub struct LinearPlugin; pub struct LinearPlayPlugin;
impl Plugin for LinearPlugin { // создаем енум, чтоб не указывать run_if(in_state) для каждой системы
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum LinearUpdateSet {
Track,
}
impl Plugin for LinearPlayPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app app
.add_systems(OnEnter (LinearState), setup) .add_systems(OnEnter(LinearPlayState), setup)
.add_systems(Update, (draw_track_gizmos).run_if(in_state(LinearState))) .add_systems(OnEnter(LinearGameRestart), linear_restart)
.add_systems(OnExit (LinearState), cleanup); .add_plugins(LinearUIPlugin)
.configure_sets(
Update,
LinearUpdateSet::Track.run_if(in_state(LinearPlayState)),
)
.add_systems(
Update,
(
draw_track_gizmos,
spawn_round_ball,
move_round_balls,
)
.in_set(LinearUpdateSet::Track),
)
.add_systems(OnExit(LinearPlayState), cleanup);
} }
} }
fn setup(mut commands: Commands) { fn setup(mut commands: Commands) {
let build_track = setup_linear_track(); let build_track = setup_linear_track();
let precalculated_track = precalculate_track(&build_track); let precalculated_track = precalculate_track(&build_track);
commands.insert_resource(build_track); commands.insert_resource(build_track);
commands.insert_resource(precalculated_track); commands.insert_resource(precalculated_track);
} }
@ -30,4 +50,9 @@ fn cleanup(mut commands: Commands, query: Query<Entity, With<LinearStateMarker>>
// зачищаем ресурсы по типу // зачищаем ресурсы по типу
commands.remove_resource::<Track>(); commands.remove_resource::<Track>();
} commands.remove_resource::<PrecalculatedTrack>();
}
fn linear_restart(mut next_state: ResMut<NextState<AppState>>) {
next_state.set(LinearPlayState);
}

View file

@ -0,0 +1,74 @@
use std::f32::consts::FRAC_PI_2;
use crate::states::linear::*;
use bevy::prelude::*;
pub fn spawn_round_ball(
track: Res<PrecalculatedTrack>,
mut commands: Commands,
asset_server: Res<AssetServer>,
balls: Query<&RoundBall>,
) {
// метод all выдаст истину, если все элементы выполняют условие
let spawn_allowed = balls
.iter()
.all(|b| b.track_progress >= track.min_spawn_gap);
let min_progress = balls
.iter()
.map(|b| b.track_progress)
.reduce(f32::min)
.unwrap_or(1.0);
if min_progress >= track.min_spawn_gap {
let ball_type = RoundBallType::random_from_4();
let image: Handle<Image> = asset_server.load(ball_type.asset_path());
commands.spawn((
Sprite {
image,
custom_size: Some(Vec2::splat(STEP)),
..default()
},
Transform::from_translation((track.segments[0].start_pos).extend(ROUND_BALL_Z)),
RoundBall {
ball_type,
track_progress: 0.0,
},
LinearStateMarker,
));
}
}
pub fn move_round_balls(
track: Res<PrecalculatedTrack>,
mut balls: Query<(&mut Transform, &mut RoundBall)>,
time: Res<Time>,
){
// собираем все существующие шарики на треке и их трансформы
for (mut transform, mut ball) in &mut balls {
// сразу обновляем прогресс
ball.track_progress += track.speed_norm * time.delta_secs();
// потом позиционируем визуал
for seg in &track.segments {
if ball.track_progress >= seg.t_start && ball.track_progress < seg.t_end {
// вычисляем локальное нормализованое положение на треке
let t_local = (ball.track_progress - seg.t_start) / (seg.t_end - seg.t_start);
let mut pos = Vec2::ZERO;
match seg.segment_type{
SegementType::Line {} => {
pos = seg.start_pos + seg.direction * seg.length * t_local;
// transform.translation = pos.extend(ROUND_BALL_Z);
}
SegementType::Turn {} => {
let angle = seg.start_angle + FRAC_PI_2 * seg.sweep_sign * t_local;
pos = seg.center + Vec2::from_angle(angle) * seg.radius;
}
}
transform.translation = pos.extend(ROUND_BALL_Z);
}
}
}
}

View file

@ -3,10 +3,6 @@ use bevy::prelude::*;
use states::linear::*; use states::linear::*;
use std::f32::consts::FRAC_PI_2; use std::f32::consts::FRAC_PI_2;
const PINK: Color = Color::srgb_u8(250, 0, 155);
const BLUE: Color = Color::srgb_u8(0, 0, 255);
const GREEN: Color = Color::srgb_u8(0, 255, 0);
pub fn draw_track_gizmos(track: Res<Track>, mut gizmos: Gizmos) { pub fn draw_track_gizmos(track: Res<Track>, mut gizmos: Gizmos) {
let mut current_pos = track.start_point; let mut current_pos = track.start_point;
let mut current_dir = track.start_direction.normalize(); let mut current_dir = track.start_direction.normalize();
@ -26,13 +22,28 @@ pub fn draw_track_gizmos(track: Res<Track>, mut gizmos: Gizmos) {
PathSegment::Turn { radius, left } => { PathSegment::Turn { radius, left } => {
// вычисляем нормаль, центр, угол поворота арки и знак // вычисляем нормаль, центр, угол поворота арки и знак
let arc = helper_arc_calculator(current_pos, current_dir, *left, *radius); let arc = helper_arc_calculator(current_pos, current_dir, *left, *radius);
// арки никак не хотели вставать на свои места
// gizmos.arc_2d(
// Isometry2d::new(arc.center, Rot2::degrees(current_angle)),
// FRAC_PI_2 * arc.sweep_sign,
// *radius,
// GREEN,
// );
// поэтому отрисовка поворота через линии
let steps = 16;
let mut prev_point = current_pos;
gizmos.arc_2d( for i in 1..=steps {
Isometry2d::new(arc.center, Rot2::radians(arc.start_angle)), let t = i as f32 / steps as f32;
FRAC_PI_2, let angle = arc.start_angle + arc.sweep_sign * FRAC_PI_2 * t;
*radius, let point = arc.center + Vec2::from_angle(angle) * *radius;
GREEN,
); gizmos.line_2d(prev_point, point, GREEN);
prev_point = point;
}
current_pos = arc.end_pos; current_pos = arc.end_pos;
current_dir = arc.normal; current_dir = arc.normal;
} }
@ -40,4 +51,4 @@ pub fn draw_track_gizmos(track: Res<Track>, mut gizmos: Gizmos) {
} }
// рисуем финиш // рисуем финиш
gizmos.circle_2d(current_pos, 5.0, Color::WHITE); gizmos.circle_2d(current_pos, 5.0, Color::WHITE);
} }

View file

@ -1,59 +1,87 @@
use std::f32::consts::FRAC_PI_2; use crate::FACTOR;
use crate::states::linear::*; use crate::states::linear::*;
use crate::{FACTOR};
use bevy::prelude::*; use bevy::prelude::*;
use bevy::reflect::List; use std::f32::consts::FRAC_PI_2;
pub const CENTER_X: f32 = 0.0;
pub const CENTER_Y: f32 = 0.0;
// pub const FACTOR_RADIUS: f32 = FACTOR as f32 / 2.0;
pub const STEP: f32 = FACTOR as f32 / SCALE;
pub const SCALE: f32 = 2.0;
pub fn setup_linear_track() -> Track { pub fn setup_linear_track() -> Track {
println!("Построение трека начато"); println!("Построение трека начато");
Track { Track {
start_point: Vec2 { start_point: Vec2 {
x: CENTER_X - FACTOR as f32 * 7.0, x: CENTER_X - FACTOR as f32 * 7.0,
y: CENTER_Y - FACTOR as f32 * 4.0, y: CENTER_Y - FACTOR as f32 * 4.0,
},
start_direction: Vec2::X,
segments: vec![
PathSegment::Line { length: STEP * 6.0 },
PathSegment::Turn {
radius: STEP,
left: true,
}, },
start_direction: Vec2::X, PathSegment::Line { length: STEP },
segments: vec![ PathSegment::Turn {
PathSegment::Line { length: STEP * 6.0 }, radius: STEP,
PathSegment::Turn { radius: STEP, left: true }, left: false,
PathSegment::Line { length: STEP }, },
PathSegment::Turn { radius: STEP, left: false }, PathSegment::Line {
PathSegment::Line { length: STEP * 18.0 }, length: STEP * 18.0,
PathSegment::Turn { radius: STEP, left: true }, },
PathSegment::Line { length: STEP * 4.0 }, PathSegment::Turn {
PathSegment::Turn { radius: STEP, left: true }, radius: STEP,
PathSegment::Turn { radius: STEP, left: false }, left: true,
PathSegment::Line { length: STEP * 4.0 }, },
PathSegment::Turn { radius: STEP, left: true }, PathSegment::Line { length: STEP * 4.0 },
PathSegment::Line { length: STEP * 20.0 }, PathSegment::Turn {
PathSegment::Turn { radius: STEP, left: true }, radius: STEP,
PathSegment::Line { length: STEP * 5.0 }, left: true,
PathSegment::Turn { radius: STEP, left: true }, },
PathSegment::Line { length: STEP * 10.0 }, PathSegment::Turn {
PathSegment::Turn { radius: STEP, left: false }, radius: STEP,
PathSegment::Line { length: STEP * 2.0 }, left: false,
], },
} PathSegment::Line { length: STEP * 4.0 },
PathSegment::Turn {
radius: STEP,
left: true,
},
PathSegment::Line {
length: STEP * 20.0,
},
PathSegment::Turn {
radius: STEP,
left: true,
},
PathSegment::Line { length: STEP * 5.0 },
PathSegment::Turn {
radius: STEP,
left: true,
},
PathSegment::Line {
length: STEP * 10.0,
},
PathSegment::Turn {
radius: STEP,
left: false,
},
PathSegment::Line { length: STEP * 2.0 },
],
}
} }
pub fn precalculate_track(track: &Track) -> PrecalculatedTrack { pub fn precalculate_track(track: &Track) -> PrecalculatedTrack {
let mut pt = PrecalculatedTrack { segments: vec![] }; let mut pt = PrecalculatedTrack {
segments: vec![],
min_spawn_gap: 0.0,
total_length: 0.0,
speed_norm: 0.0,
};
let mut cumulative_length: Vec<f32> = vec![0.0]; let mut cumulative_length: Vec<f32> = vec![0.0];
let mut current_pos = track.start_point; let mut current_pos = track.start_point;
let mut current_dir = track.start_direction; let mut current_dir = track.start_direction;
// на первом проходе заполняем все, кроме t_start и t_end, // на первом проходе заполняем все, кроме t_start и t_end,
// потому что мы не можем посчитать их без общей длины // потому что мы не можем посчитать их без общей длины
for seg in track.segments.iter(){ for seg in track.segments.iter() {
match seg { match seg {
PathSegment::Line { length } => { PathSegment::Line { length } => {
let calc_seg = PTSegment { let calc_seg = PTSegment {
@ -66,6 +94,7 @@ pub fn precalculate_track(track: &Track) -> PrecalculatedTrack {
radius: 0.0, radius: 0.0,
start_angle: 0.0, start_angle: 0.0,
sweep_sign: 0.0, sweep_sign: 0.0,
length: *length,
}; };
pt.segments.push(calc_seg); pt.segments.push(calc_seg);
@ -73,12 +102,13 @@ pub fn precalculate_track(track: &Track) -> PrecalculatedTrack {
current_pos = current_pos + length * current_dir; current_pos = current_pos + length * current_dir;
// направление не меняем, пропуск // направление не меняем, пропуск
// накапливаем длину // накапливаем длину
cumulative_length.push(cumulative_length[cumulative_length.len() -1] + *length); cumulative_length.push(cumulative_length[cumulative_length.len() - 1] + *length);
} }
PathSegment::Turn {radius, left } => { PathSegment::Turn { radius, left } => {
let arc = helper_arc_calculator(current_pos, current_dir, *left, *radius); let arc = helper_arc_calculator(current_pos, current_dir, *left, *radius);
let arc_len = FRAC_PI_2 * radius;
let calc_seg = PTSegment { let calc_seg = PTSegment {
t_start: 0.0, t_start: 0.0,
t_end: 0.0, t_end: 0.0,
@ -89,53 +119,63 @@ pub fn precalculate_track(track: &Track) -> PrecalculatedTrack {
radius: *radius, radius: *radius,
start_angle: arc.start_angle, start_angle: arc.start_angle,
sweep_sign: arc.sweep_sign, sweep_sign: arc.sweep_sign,
length: arc_len,
}; };
pt.segments.push(calc_seg); pt.segments.push(calc_seg);
// сдвигаем позицию для следующего сегмента // сдвигаем позицию для следующего сегмента
current_pos = arc.end_pos; current_pos = arc.end_pos;
current_dir = arc.normal; current_dir = arc.normal;
// накапливаем длину // накапливаем длину
cumulative_length.push(cumulative_length[cumulative_length.len() -1] + (FRAC_PI_2 * radius)); cumulative_length.push(cumulative_length[cumulative_length.len() - 1] + arc_len);
} }
} }
} }
let last_index = cumulative_length.len() - 1; let last_index = cumulative_length.len() - 1;
let total_length = cumulative_length[last_index]; let total_length = cumulative_length[last_index];
// итерируемся уже по прекалькулированному вектору // итерируемся уже по прекалькулированному вектору
// заполняем точки на треке в пересчете на общую длину // заполняем точки на треке в пересчете на общую длину
println!("Precalculated track:"); println!("Precalculated track:");
for (i, seg) in pt.segments.iter_mut().enumerate() { for (i, seg) in pt.segments.iter_mut().enumerate() {
seg.t_start = cumulative_length[i] / total_length; seg.t_start = cumulative_length[i] / total_length;
if i < last_index { if i < last_index {
seg.t_end = cumulative_length[i+1] / total_length; seg.t_end = cumulative_length[i + 1] / total_length;
} else { } else {
seg.t_end = 1.0; seg.t_end = 1.0;
} }
println!("{:?}", seg); println!("{:?}", seg);
} }
// считаем нормализованный зазор на треке, когда будет доступен спавн шарика
// pt.min_spawn_gap = STEP / total_length;
pt.min_spawn_gap = STEP / total_length;
pt.total_length = total_length;
pt.speed_norm = STEP / total_length; // нормализованное смещение для скорости в пикселях
println!(
"Нормализованное значение размера шарика: {}, уе: {}, общая длина: {}",
pt.min_spawn_gap, STEP, total_length
);
pt pt
} }
pub fn helper_arc_calculator(pos: Vec2, dir: Vec2, turn_to: bool, radius: f32) -> ArcCalculation{ pub fn helper_arc_calculator(pos: Vec2, dir: Vec2, turn_to: bool, radius: f32) -> ArcCalculation {
let (normal, sign): (Vec2, f32) = if turn_to { let (normal, sign): (Vec2, f32) = if turn_to {
(Vec2::new(-dir.y, dir.x), -1.0) (Vec2::new(-dir.y, dir.x), 1.0)
} else { } else {
(Vec2::new(dir.y, -dir.x), 1.0) (Vec2::new(dir.y, -dir.x), -1.0)
}; };
let center = pos + normal * radius; let center = pos + normal * radius;
let start_vec = pos - center; let start_vec = pos - center;
let initial_vec = if turn_to { -start_vec.perp() } else { -start_vec }; let angle = start_vec.to_angle();
let angle = initial_vec.to_angle();
ArcCalculation {
ArcCalculation{
normal, normal,
center, center,
start_angle: angle, start_angle: angle,
sweep_sign: sign, sweep_sign: sign,
end_pos: center + normal.perp() * radius * sign, end_pos: center - normal.perp() * radius * sign,
} }
} }

77
src/states/linear/ui.rs Normal file
View file

@ -0,0 +1,77 @@
use crate::states::AppState;
use crate::states::AppState::LinearPlayState;
use crate::states::linear::plugin::LinearUpdateSet;
use crate::states::linear::{LinearRestartMarker, LinearStateMarker};
use crate::ui::button_click::ButtonClickMessage;
use crate::ui::click::handle_click_system;
use crate::ui::{ButtonStyle, spawn_button};
use bevy::prelude::*;
pub struct LinearUIPlugin;
impl Plugin for LinearUIPlugin {
fn build(&self, app: &mut App) {
app.add_message::<ButtonClickMessage<LinearRestartMarker>>()
.add_systems(OnEnter(LinearPlayState), setup_ui)
.configure_sets(
Update,
LinearUpdateSet::Track.run_if(in_state(LinearPlayState)),
)
.add_systems(
Update,
(
restart_game_button_system,
handle_click_system::<LinearRestartMarker>,
)
.in_set(LinearUpdateSet::Track),
);
}
}
// хоть ui отделен, но по глобальному маркеру стейта, все сущности будут очищены в основном плагине
fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
let root = commands
.spawn((
Node {
display: Display::Flex,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
BackgroundColor(Color::NONE),
Name::new("Game state background node"),
LinearStateMarker,
))
.id();
let restart_button_style = ButtonStyle {
font: asset_server.load("fonts/QR Ames Beta.otf"),
font_size: 24.0,
text_color: Color::WHITE,
normal_bg: Color::linear_rgba(0.15, 0.15, 0.15, 1.0),
hovered_bg: Color::linear_rgb(0.25, 0.25, 0.25),
pressed_bg: Color::linear_rgb(0.3, 0.3, 0.3),
width: Val::Px(200.0),
height: Val::Px(50.0),
margin: UiRect::all(Val::Px(10.0)),
};
spawn_button(
&mut commands,
root,
"Restart",
&restart_button_style,
LinearRestartMarker,
);
}
pub fn restart_game_button_system(
interaction_query: Query<&Interaction, (Changed<Interaction>, With<LinearRestartMarker>)>,
mut next_state: ResMut<NextState<AppState>>,
) {
for interaction in &interaction_query {
if matches!(interaction, Interaction::Pressed) {
next_state.set(AppState::LinearGameRestart);
}
}
}

View file

@ -31,7 +31,7 @@ pub fn linear_button_system(
for interaction in &interaction_query { for interaction in &interaction_query {
if matches!(interaction, Interaction::Pressed) { if matches!(interaction, Interaction::Pressed) {
println!("Линейное нажата"); println!("Линейное нажата");
next_state.set(AppState::LinearState); next_state.set(AppState::LinearPlayState);
} }
} }
} }