factor ou projectile components and systems
This commit is contained in:
parent
eeebdbd272
commit
67ddcb0c65
6 changed files with 426 additions and 293 deletions
|
|
@ -31,33 +31,9 @@ impl LinearCannonState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct RoundBallProjectile {
|
|
||||||
pub velocity: Vec2,
|
|
||||||
pub direction: Vec2,
|
|
||||||
pub previous_position: Vec2,
|
|
||||||
pub ball_type: RoundBallType,
|
|
||||||
pub hit_result: Option<ProjectileHit>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ProjectileHit {
|
|
||||||
pub hit_progress: f32,
|
|
||||||
pub hit_position: Vec2,
|
|
||||||
pub segment_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct ShotMarker;
|
pub struct ShotMarker;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct SwapMarker;
|
pub struct SwapMarker;
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct IntersectMarker;
|
|
||||||
|
|
||||||
pub struct Intersection {
|
|
||||||
pub t: f32,
|
|
||||||
pub world_pos: Vec2,
|
|
||||||
pub segment_index: usize,
|
|
||||||
}
|
|
||||||
33
src/states/linear/components_projectile.rs
Normal file
33
src/states/linear/components_projectile.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use bevy::math::Vec2;
|
||||||
|
use bevy::prelude::Component;
|
||||||
|
use crate::states::linear::RoundBallType;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct RoundBallProjectile {
|
||||||
|
pub velocity: Vec2,
|
||||||
|
pub direction: Vec2,
|
||||||
|
pub previous_position: Vec2,
|
||||||
|
pub ball_type: RoundBallType,
|
||||||
|
pub hit_result: Option<ProjectileHit>,
|
||||||
|
pub target_position: Option<Vec2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct ProjectileHit {
|
||||||
|
pub hit_progress: f32,
|
||||||
|
pub hit_position: Vec2,
|
||||||
|
pub segment_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct IntersectMarker;
|
||||||
|
|
||||||
|
pub struct Intersection {
|
||||||
|
pub t: f32,
|
||||||
|
pub world_pos: Vec2,
|
||||||
|
pub segment_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct InsertMarker;
|
||||||
|
|
@ -25,6 +25,11 @@ mod components_cannon;
|
||||||
pub use components_cannon::*;
|
pub use components_cannon::*;
|
||||||
mod systems_cannon;
|
mod systems_cannon;
|
||||||
pub use systems_cannon::*;
|
pub use systems_cannon::*;
|
||||||
|
mod systems_projectile;
|
||||||
|
pub use systems_projectile::*;
|
||||||
|
mod components_projectile;
|
||||||
|
pub use components_projectile::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ impl Plugin for LinearPlayPlugin {
|
||||||
draw_track_gizmos,
|
draw_track_gizmos,
|
||||||
draw_grid,
|
draw_grid,
|
||||||
// draw_cannon_laser,
|
// draw_cannon_laser,
|
||||||
//несколько систем с соблюдением порядка
|
|
||||||
cycle_cannon_balls,
|
cycle_cannon_balls,
|
||||||
update_linear_cannon_preview,
|
update_linear_cannon_preview,
|
||||||
spawn_projectile_from_cannon,
|
spawn_projectile_from_cannon,
|
||||||
|
|
@ -47,6 +46,7 @@ impl Plugin for LinearPlayPlugin {
|
||||||
move_round_balls,
|
move_round_balls,
|
||||||
calculate_projectile_hits,
|
calculate_projectile_hits,
|
||||||
linear_move_projectiles,
|
linear_move_projectiles,
|
||||||
|
insert_projectile_into_track,
|
||||||
).in_set(LinearUpdateSet::Track),
|
).in_set(LinearUpdateSet::Track),
|
||||||
)
|
)
|
||||||
.add_systems(OnExit(LinearPlayState), cleanup);
|
.add_systems(OnExit(LinearPlayState), cleanup);
|
||||||
|
|
@ -62,8 +62,8 @@ fn setup(mut commands: Commands) {
|
||||||
commands.insert_resource(build_linear_cannon_state());
|
commands.insert_resource(build_linear_cannon_state());
|
||||||
|
|
||||||
let debug_config = LinearDebugConfig {
|
let debug_config = LinearDebugConfig {
|
||||||
toggle_track: true,
|
toggle_track: false,
|
||||||
toggle_grid: true,
|
toggle_grid: false,
|
||||||
toggle_laser: false,
|
toggle_laser: false,
|
||||||
};
|
};
|
||||||
commands.insert_resource(debug_config);
|
commands.insert_resource(debug_config);
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ pub fn spawn_projectile_from_cannon(
|
||||||
previous_position: spawn_pos_2d,
|
previous_position: spawn_pos_2d,
|
||||||
ball_type,
|
ball_type,
|
||||||
hit_result: None,
|
hit_result: None,
|
||||||
|
target_position: None,
|
||||||
},
|
},
|
||||||
IntersectMarker, // маркер для фильтрации, что шарик-снаряд может иметь пересечения с треком
|
IntersectMarker, // маркер для фильтрации, что шарик-снаряд может иметь пересечения с треком
|
||||||
LinearStateMarker,
|
LinearStateMarker,
|
||||||
|
|
@ -180,269 +181,3 @@ pub fn cycle_cannon_balls(
|
||||||
cannon_state.cycle();
|
cannon_state.cycle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn linear_move_projectiles(
|
|
||||||
mut commands: Commands,
|
|
||||||
mut projectiles: Query<(Entity, &mut RoundBallProjectile, &mut Transform)>,
|
|
||||||
time: Res<Time>,
|
|
||||||
) {
|
|
||||||
for (entity, mut proj, mut transform) in projectiles.iter_mut() {
|
|
||||||
let delta = proj.velocity * time.delta_secs();
|
|
||||||
let new_pos = proj.previous_position + delta;
|
|
||||||
|
|
||||||
transform.translation = new_pos.extend(transform.translation.z);
|
|
||||||
proj.previous_position = new_pos;
|
|
||||||
|
|
||||||
// удаляем снаряды, улетевшие за экран
|
|
||||||
if new_pos.x.abs() > 2.0 * (WIDTH * FACTOR) as f32
|
|
||||||
|| new_pos.y.abs() > 2.0 * (HEIGHT * FACTOR) as f32
|
|
||||||
{
|
|
||||||
commands.entity(entity).despawn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn calculate_projectile_hits(
|
|
||||||
mut commands: Commands,
|
|
||||||
mut projectiles: Query<
|
|
||||||
(Entity, &mut RoundBallProjectile, &mut Transform),
|
|
||||||
With<IntersectMarker>,
|
|
||||||
>,
|
|
||||||
track: Res<PrecalculatedTrack>,
|
|
||||||
balls_on_track: Query<&RoundBall>,
|
|
||||||
) {
|
|
||||||
for (entity, mut proj, mut transform) in projectiles.iter_mut() {
|
|
||||||
// если уже посчитано место попадания, пропускаем
|
|
||||||
if proj.hit_result.is_some() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ищем все точки пересечений с треком
|
|
||||||
let mut intersections = raycast_track(proj.previous_position, proj.direction, &track);
|
|
||||||
|
|
||||||
// сортируем от ближайшего t к дальнему
|
|
||||||
intersections.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap());
|
|
||||||
|
|
||||||
// если есть точки пересечения
|
|
||||||
if let Some(closest_hit) = intersections.first() {
|
|
||||||
// собираем шарики
|
|
||||||
let mut occupied: Vec<f32> = balls_on_track.iter().map(|b| b.track_progress).collect();
|
|
||||||
// сортируем прогресс для дальнейших вычислений
|
|
||||||
occupied.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
||||||
|
|
||||||
//считаем прогресс для точки попадания через мировые координаты и индекс сегмента на треке
|
|
||||||
let hit_progress = calculate_progress(closest_hit, &track);
|
|
||||||
|
|
||||||
let radius_norm = BALL_RADIUS / track.total_length;
|
|
||||||
if is_occupied(hit_progress, &occupied, radius_norm) {
|
|
||||||
proj.hit_result = Some(ProjectileHit {
|
|
||||||
hit_progress,
|
|
||||||
hit_position: closest_hit.world_pos,
|
|
||||||
segment_index: closest_hit.segment_index,
|
|
||||||
});
|
|
||||||
println!("Попадание в трек: {}", hit_progress);
|
|
||||||
transform.translation = closest_hit.world_pos.extend(transform.translation.z);
|
|
||||||
// убираем маркер поиска
|
|
||||||
commands.entity(entity).remove::<IntersectMarker>();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Пересечений нет -> убираем маркер, снаряд летит дальше
|
|
||||||
commands.entity(entity).remove::<IntersectMarker>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn raycast_track(origin: Vec2, direction: Vec2, track: &PrecalculatedTrack) -> Vec<Intersection> {
|
|
||||||
let mut intersections = vec![];
|
|
||||||
|
|
||||||
for (i, seg) in track.segments.iter().enumerate() {
|
|
||||||
match seg.segment_type {
|
|
||||||
SegementType::Line => {
|
|
||||||
// ищем пересечение
|
|
||||||
if let Some(t) =
|
|
||||||
intersect_line(origin, direction, seg.start_pos, seg.direction, seg.length)
|
|
||||||
{
|
|
||||||
let pos = origin + direction * t;
|
|
||||||
intersections.push(Intersection {
|
|
||||||
t,
|
|
||||||
world_pos: pos,
|
|
||||||
segment_index: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SegementType::Turn => {
|
|
||||||
let hits = intersect_arc(
|
|
||||||
origin,
|
|
||||||
direction,
|
|
||||||
seg.center,
|
|
||||||
seg.radius,
|
|
||||||
seg.start_angle,
|
|
||||||
seg.sweep_sign,
|
|
||||||
);
|
|
||||||
|
|
||||||
for t in hits {
|
|
||||||
let pos = origin + direction * t;
|
|
||||||
intersections.push(Intersection {
|
|
||||||
t,
|
|
||||||
world_pos: pos,
|
|
||||||
segment_index: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
intersections
|
|
||||||
}
|
|
||||||
|
|
||||||
fn intersect_line(
|
|
||||||
origin: Vec2,
|
|
||||||
direction: Vec2,
|
|
||||||
seg_start: Vec2,
|
|
||||||
seg_dir: Vec2,
|
|
||||||
seg_length: f32,
|
|
||||||
) -> Option<f32> {
|
|
||||||
// вектор самого отрезка трека
|
|
||||||
let seg_vec = seg_dir * seg_length;
|
|
||||||
|
|
||||||
// знаменатель: векторное произведение направления луча и вектора отрезка
|
|
||||||
// x1*y2 - x2*y1
|
|
||||||
let denom = direction.x * seg_vec.y - direction.y * seg_vec.x;
|
|
||||||
|
|
||||||
// если знаменатель близок к нулю - луч параллелен отрезку, пересечения нет
|
|
||||||
if denom.abs() < 1e-6 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let w = seg_start - origin;
|
|
||||||
|
|
||||||
// вычисляем параметры t и u
|
|
||||||
// t - расстояние по лучу
|
|
||||||
// u - позиция на отрезке (0.0 = начало, 1.0 = конец)
|
|
||||||
let t = (w.x * seg_vec.y - w.y * seg_vec.x) / denom;
|
|
||||||
let u = (w.x * direction.y - w.y * direction.x) / denom;
|
|
||||||
|
|
||||||
// проверяем условия попадания
|
|
||||||
// t > 0 - точка пересечения находится перед пушкой (а не за спиной)
|
|
||||||
// u >= 0 && u <= 1 - точка пересечения находится внутри отрезка (не на бесконечной линии)
|
|
||||||
if t > 0.0 && u >= 0.0 && u <= 1.0 {
|
|
||||||
return Some(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn intersect_arc(
|
|
||||||
origin: Vec2,
|
|
||||||
direction: Vec2,
|
|
||||||
center: Vec2,
|
|
||||||
radius: f32,
|
|
||||||
start_angle: f32,
|
|
||||||
sweep_sign: f32,
|
|
||||||
) -> Vec<f32> {
|
|
||||||
// возвращает список дистанций t до точек пересечения с дугой.
|
|
||||||
// может вернуть 0, 1 или 2 точки (вход и выход), если луч проходит сквозь дугу.
|
|
||||||
|
|
||||||
let mut hits = Vec::new();
|
|
||||||
|
|
||||||
// считаем пересечения луча и полной окружности через квадратное уравнение
|
|
||||||
|
|
||||||
let l = origin - center;
|
|
||||||
let b = 2.0 * l.dot(direction); // поскольку направление нормализовано, a = 1
|
|
||||||
let c = l.dot(l) - radius * radius;
|
|
||||||
|
|
||||||
let discriminant = b * b - 4.0 * c;
|
|
||||||
|
|
||||||
// если дискриминант меньше нуля, луч пролетает мимо окружности
|
|
||||||
if discriminant < 0.0 {
|
|
||||||
return hits;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sqrt_discriminant = discriminant.sqrt();
|
|
||||||
|
|
||||||
// находим корни
|
|
||||||
let roots = vec![
|
|
||||||
(-b - sqrt_discriminant) / 2.0,
|
|
||||||
(-b + sqrt_discriminant) / 2.0,
|
|
||||||
];
|
|
||||||
|
|
||||||
for t in roots {
|
|
||||||
// ищем только положительные точки, потому что они будут перед пушкой
|
|
||||||
if t > 0.0 {
|
|
||||||
let hit_point = origin + direction * t;
|
|
||||||
|
|
||||||
// попали ли в сектор дуги
|
|
||||||
let hit_angle = (hit_point - center).to_angle();
|
|
||||||
|
|
||||||
// находим разницу между углом попадания и стартовым углом
|
|
||||||
let mut angle_diff = hit_angle - start_angle;
|
|
||||||
|
|
||||||
// нормализуем разницу в диапазон [-PI, PI]
|
|
||||||
// это нужно, чтобы корректно обрабатывать переход через плюс/минус пи
|
|
||||||
while angle_diff > PI {
|
|
||||||
angle_diff -= 2.0 * PI;
|
|
||||||
}
|
|
||||||
while angle_diff < -PI {
|
|
||||||
angle_diff += 2.0 * PI;
|
|
||||||
}
|
|
||||||
|
|
||||||
// проверяем, лежит ли разница в пределах 90 градусов (пи пополам) с учетом направления
|
|
||||||
let is_valid_angle = if sweep_sign > 0.0 {
|
|
||||||
// против часовой
|
|
||||||
angle_diff >= 0.0 && angle_diff <= FRAC_PI_2
|
|
||||||
} else {
|
|
||||||
// по часовой
|
|
||||||
angle_diff >= -FRAC_PI_2 && angle_diff <= 0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
if is_valid_angle {
|
|
||||||
hits.push(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hits
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_progress(inter: &Intersection, track: &PrecalculatedTrack) -> f32 {
|
|
||||||
// берем только нужный сегмент трека, где есть пересечение
|
|
||||||
let track_seg = &track.segments[inter.segment_index];
|
|
||||||
|
|
||||||
// по типу сегмента переводим точку из мировых координат Vec2 в прогресс трека 0.0 ... 1.0
|
|
||||||
match track_seg.segment_type {
|
|
||||||
SegementType::Line => {
|
|
||||||
let diff = inter.world_pos - track_seg.start_pos;
|
|
||||||
let local_t = (diff.x * track_seg.direction.x + diff.y * track_seg.direction.y)
|
|
||||||
/ track_seg.length;
|
|
||||||
// возвращаем прогресс
|
|
||||||
track_seg.t_start + (track_seg.t_end - track_seg.t_start) * local_t
|
|
||||||
}
|
|
||||||
|
|
||||||
SegementType::Turn => {
|
|
||||||
let hit_angle = (inter.world_pos - track_seg.center).to_angle();
|
|
||||||
let mut angle_diff = hit_angle - track_seg.start_angle;
|
|
||||||
|
|
||||||
// критичный момент - важно нормализовать разницу в диапазон от -пи до +пи, чтоб он не улетел
|
|
||||||
while angle_diff > PI {
|
|
||||||
angle_diff -= 2.0 * PI
|
|
||||||
}
|
|
||||||
while angle_diff < -PI {
|
|
||||||
angle_diff += 2.0 * PI
|
|
||||||
}
|
|
||||||
|
|
||||||
// учитываем направление обхода
|
|
||||||
let local_t = angle_diff / (track_seg.sweep_sign * FRAC_PI_2);
|
|
||||||
|
|
||||||
// возвращаем прогресс
|
|
||||||
track_seg.t_start + (track_seg.t_end - track_seg.t_start) * local_t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_occupied(target: f32, occupied: &[f32], radius_norm: f32) -> bool {
|
|
||||||
let min = target - radius_norm;
|
|
||||||
let max = target + radius_norm;
|
|
||||||
|
|
||||||
occupied.iter().any(|&p| p >= min && p <= max)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
384
src/states/linear/systems_projectile.rs
Normal file
384
src/states/linear/systems_projectile.rs
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
use crate::states::linear::*;
|
||||||
|
use crate::{FACTOR, HEIGHT, WIDTH};
|
||||||
|
use bevy::asset::AssetServer;
|
||||||
|
use bevy::math::Vec2;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use std::f32::consts::{FRAC_PI_2, PI};
|
||||||
|
|
||||||
|
pub fn linear_move_projectiles(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut projectiles: Query<(Entity, &mut RoundBallProjectile, &mut Transform)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (entity, mut proj, mut transform) in projectiles.iter_mut() {
|
||||||
|
let delta = proj.velocity * time.delta_secs();
|
||||||
|
let new_pos = proj.previous_position + delta;
|
||||||
|
|
||||||
|
transform.translation = new_pos.extend(transform.translation.z);
|
||||||
|
proj.previous_position = new_pos;
|
||||||
|
|
||||||
|
// удаляем снаряды, улетевшие за экран
|
||||||
|
if new_pos.x.abs() > 2.0 * (WIDTH * FACTOR) as f32
|
||||||
|
|| new_pos.y.abs() > 2.0 * (HEIGHT * FACTOR) as f32
|
||||||
|
{
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_projectile_hits(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut projectiles: Query<
|
||||||
|
(Entity, &mut RoundBallProjectile, &mut Transform),
|
||||||
|
With<IntersectMarker>,
|
||||||
|
>,
|
||||||
|
track: Res<PrecalculatedTrack>,
|
||||||
|
balls_on_track: Query<&RoundBall>,
|
||||||
|
) {
|
||||||
|
for (entity, mut proj, mut transform) in projectiles.iter_mut() {
|
||||||
|
// если уже посчитано место попадания, пропускаем
|
||||||
|
if proj.hit_result.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ищем все точки пересечений с треком
|
||||||
|
let mut intersections = raycast_track(proj.previous_position, proj.direction, &track);
|
||||||
|
|
||||||
|
// сортируем от ближайшего t к дальнему
|
||||||
|
intersections.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap());
|
||||||
|
|
||||||
|
// // если есть точки пересечения
|
||||||
|
// if let Some(closest_hit) = intersections.first() {
|
||||||
|
// // собираем шарики
|
||||||
|
// let mut occupied: Vec<f32> = balls_on_track.iter().map(|b| b.track_progress).collect();
|
||||||
|
// // сортируем прогресс для дальнейших вычислений
|
||||||
|
// occupied.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
//
|
||||||
|
// //считаем прогресс для точки попадания через мировые координаты и индекс сегмента на треке
|
||||||
|
// let hit_progress = calculate_progress(closest_hit, &track);
|
||||||
|
//
|
||||||
|
// let radius_norm = BALL_RADIUS / track.total_length;
|
||||||
|
// if is_occupied(hit_progress, &occupied, radius_norm) {
|
||||||
|
// proj.hit_result = Some(ProjectileHit {
|
||||||
|
// hit_progress,
|
||||||
|
// hit_position: closest_hit.world_pos,
|
||||||
|
// segment_index: closest_hit.segment_index,
|
||||||
|
// });
|
||||||
|
// println!("Попадание в трек: {}", hit_progress);
|
||||||
|
// transform.translation = closest_hit.world_pos.extend(transform.translation.z);
|
||||||
|
// // убираем маркер поиска, добавляем маркер готовности ко вставке для другой системы
|
||||||
|
// commands
|
||||||
|
// .entity(entity)
|
||||||
|
// .remove::<IntersectMarker>()
|
||||||
|
// .insert(InsertMarker);
|
||||||
|
// }
|
||||||
|
let mut occupied: Vec<f32> = balls_on_track.iter().map(|b| b.track_progress).collect();
|
||||||
|
occupied.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
let radius_norm = BALL_RADIUS / track.total_length;
|
||||||
|
|
||||||
|
let mut target_hit = None;
|
||||||
|
for inter in &intersections {
|
||||||
|
let hit_progress = calculate_progress(inter, &track);
|
||||||
|
|
||||||
|
if is_occupied(hit_progress, &occupied, radius_norm) {
|
||||||
|
target_hit = Some((inter, hit_progress));
|
||||||
|
break; // Нашли цель — выходим из цикла
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((target_inter, hit_progress)) = target_hit {
|
||||||
|
// ПОПАДАНИЕ: снэпим именно к той точке, которая занята!
|
||||||
|
proj.hit_result = Some(ProjectileHit {
|
||||||
|
hit_progress,
|
||||||
|
hit_position: target_inter.world_pos,
|
||||||
|
segment_index: target_inter.segment_index,
|
||||||
|
});
|
||||||
|
|
||||||
|
// записываем координаты точки попадания, до которой летит шарик
|
||||||
|
// вероятен момент, когда шарик летит, а в точке пересечения появится шарики на треке
|
||||||
|
// если вылезет такое, пофиксить через проверку всех точек пересечения, а не только
|
||||||
|
// конкретной, которая тут записана
|
||||||
|
proj.target_position = Some(target_inter.world_pos);
|
||||||
|
|
||||||
|
// Переключаем маркер: больше не ищем, готовы к вставке
|
||||||
|
commands.entity(entity)
|
||||||
|
.remove::<IntersectMarker>()
|
||||||
|
.insert(InsertMarker);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Пересечений нет -> убираем маркер, снаряд летит дальше
|
||||||
|
commands.entity(entity).remove::<IntersectMarker>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raycast_track(origin: Vec2, direction: Vec2, track: &PrecalculatedTrack) -> Vec<Intersection> {
|
||||||
|
let mut intersections = vec![];
|
||||||
|
|
||||||
|
for (i, seg) in track.segments.iter().enumerate() {
|
||||||
|
match seg.segment_type {
|
||||||
|
SegementType::Line => {
|
||||||
|
// ищем пересечение
|
||||||
|
if let Some(t) =
|
||||||
|
intersect_line(origin, direction, seg.start_pos, seg.direction, seg.length)
|
||||||
|
{
|
||||||
|
let pos = origin + direction * t;
|
||||||
|
intersections.push(Intersection {
|
||||||
|
t,
|
||||||
|
world_pos: pos,
|
||||||
|
segment_index: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SegementType::Turn => {
|
||||||
|
let hits = intersect_arc(
|
||||||
|
origin,
|
||||||
|
direction,
|
||||||
|
seg.center,
|
||||||
|
seg.radius,
|
||||||
|
seg.start_angle,
|
||||||
|
seg.sweep_sign,
|
||||||
|
);
|
||||||
|
|
||||||
|
for t in hits {
|
||||||
|
let pos = origin + direction * t;
|
||||||
|
intersections.push(Intersection {
|
||||||
|
t,
|
||||||
|
world_pos: pos,
|
||||||
|
segment_index: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
intersections
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intersect_line(
|
||||||
|
origin: Vec2,
|
||||||
|
direction: Vec2,
|
||||||
|
seg_start: Vec2,
|
||||||
|
seg_dir: Vec2,
|
||||||
|
seg_length: f32,
|
||||||
|
) -> Option<f32> {
|
||||||
|
// вектор самого отрезка трека
|
||||||
|
let seg_vec = seg_dir * seg_length;
|
||||||
|
|
||||||
|
// знаменатель: векторное произведение направления луча и вектора отрезка
|
||||||
|
// x1*y2 - x2*y1
|
||||||
|
let denom = direction.x * seg_vec.y - direction.y * seg_vec.x;
|
||||||
|
|
||||||
|
// если знаменатель близок к нулю - луч параллелен отрезку, пересечения нет
|
||||||
|
if denom.abs() < 1e-6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let w = seg_start - origin;
|
||||||
|
|
||||||
|
// вычисляем параметры t и u
|
||||||
|
// t - расстояние по лучу
|
||||||
|
// u - позиция на отрезке (0.0 = начало, 1.0 = конец)
|
||||||
|
let t = (w.x * seg_vec.y - w.y * seg_vec.x) / denom;
|
||||||
|
let u = (w.x * direction.y - w.y * direction.x) / denom;
|
||||||
|
|
||||||
|
// проверяем условия попадания
|
||||||
|
// t > 0 - точка пересечения находится перед пушкой (а не за спиной)
|
||||||
|
// u >= 0 && u <= 1 - точка пересечения находится внутри отрезка (не на бесконечной линии)
|
||||||
|
if t > 0.0 && u >= 0.0 && u <= 1.0 {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intersect_arc(
|
||||||
|
origin: Vec2,
|
||||||
|
direction: Vec2,
|
||||||
|
center: Vec2,
|
||||||
|
radius: f32,
|
||||||
|
start_angle: f32,
|
||||||
|
sweep_sign: f32,
|
||||||
|
) -> Vec<f32> {
|
||||||
|
// возвращает список дистанций t до точек пересечения с дугой.
|
||||||
|
// может вернуть 0, 1 или 2 точки (вход и выход), если луч проходит сквозь дугу.
|
||||||
|
|
||||||
|
let mut hits = Vec::new();
|
||||||
|
|
||||||
|
// считаем пересечения луча и полной окружности через квадратное уравнение
|
||||||
|
|
||||||
|
let l = origin - center;
|
||||||
|
let b = 2.0 * l.dot(direction); // поскольку направление нормализовано, a = 1
|
||||||
|
let c = l.dot(l) - radius * radius;
|
||||||
|
|
||||||
|
let discriminant = b * b - 4.0 * c;
|
||||||
|
|
||||||
|
// если дискриминант меньше нуля, луч пролетает мимо окружности
|
||||||
|
if discriminant < 0.0 {
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sqrt_discriminant = discriminant.sqrt();
|
||||||
|
|
||||||
|
// находим корни
|
||||||
|
let roots = vec![
|
||||||
|
(-b - sqrt_discriminant) / 2.0,
|
||||||
|
(-b + sqrt_discriminant) / 2.0,
|
||||||
|
];
|
||||||
|
|
||||||
|
for t in roots {
|
||||||
|
// ищем только положительные точки, потому что они будут перед пушкой
|
||||||
|
if t > 0.0 {
|
||||||
|
let hit_point = origin + direction * t;
|
||||||
|
|
||||||
|
// попали ли в сектор дуги
|
||||||
|
let hit_angle = (hit_point - center).to_angle();
|
||||||
|
|
||||||
|
// находим разницу между углом попадания и стартовым углом
|
||||||
|
let mut angle_diff = hit_angle - start_angle;
|
||||||
|
|
||||||
|
// нормализуем разницу в диапазон [-PI, PI]
|
||||||
|
// это нужно, чтобы корректно обрабатывать переход через плюс/минус пи
|
||||||
|
while angle_diff > PI {
|
||||||
|
angle_diff -= 2.0 * PI;
|
||||||
|
}
|
||||||
|
while angle_diff < -PI {
|
||||||
|
angle_diff += 2.0 * PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
// проверяем, лежит ли разница в пределах 90 градусов (пи пополам) с учетом направления
|
||||||
|
let is_valid_angle = if sweep_sign > 0.0 {
|
||||||
|
// против часовой
|
||||||
|
angle_diff >= 0.0 && angle_diff <= FRAC_PI_2
|
||||||
|
} else {
|
||||||
|
// по часовой
|
||||||
|
angle_diff >= -FRAC_PI_2 && angle_diff <= 0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_valid_angle {
|
||||||
|
hits.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hits
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_progress(inter: &Intersection, track: &PrecalculatedTrack) -> f32 {
|
||||||
|
// берем только нужный сегмент трека, где есть пересечение
|
||||||
|
let track_seg = &track.segments[inter.segment_index];
|
||||||
|
|
||||||
|
// по типу сегмента переводим точку из мировых координат Vec2 в прогресс трека 0.0 ... 1.0
|
||||||
|
match track_seg.segment_type {
|
||||||
|
SegementType::Line => {
|
||||||
|
let diff = inter.world_pos - track_seg.start_pos;
|
||||||
|
let local_t = (diff.x * track_seg.direction.x + diff.y * track_seg.direction.y)
|
||||||
|
/ track_seg.length;
|
||||||
|
// возвращаем прогресс
|
||||||
|
track_seg.t_start + (track_seg.t_end - track_seg.t_start) * local_t
|
||||||
|
}
|
||||||
|
|
||||||
|
SegementType::Turn => {
|
||||||
|
let hit_angle = (inter.world_pos - track_seg.center).to_angle();
|
||||||
|
let mut angle_diff = hit_angle - track_seg.start_angle;
|
||||||
|
|
||||||
|
// критичный момент - важно нормализовать разницу в диапазон от -пи до +пи, чтоб он не улетел
|
||||||
|
while angle_diff > PI {
|
||||||
|
angle_diff -= 2.0 * PI
|
||||||
|
}
|
||||||
|
while angle_diff < -PI {
|
||||||
|
angle_diff += 2.0 * PI
|
||||||
|
}
|
||||||
|
|
||||||
|
// учитываем направление обхода
|
||||||
|
let local_t = angle_diff / (track_seg.sweep_sign * FRAC_PI_2);
|
||||||
|
|
||||||
|
// возвращаем прогресс
|
||||||
|
track_seg.t_start + (track_seg.t_end - track_seg.t_start) * local_t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_occupied(target: f32, occupied: &[f32], radius_norm: f32) -> bool {
|
||||||
|
let min = target - radius_norm;
|
||||||
|
let max = target + radius_norm;
|
||||||
|
|
||||||
|
occupied.iter().any(|&p| p >= min && p <= max)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_projectile_into_track(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut projectiles: Query<(Entity, &RoundBallProjectile), With<InsertMarker>>,
|
||||||
|
mut balls_on_track: Query<(Entity, &mut RoundBall)>,
|
||||||
|
track: Res<PrecalculatedTrack>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
) {
|
||||||
|
for (proj_entity, proj) in projectiles.iter_mut() {
|
||||||
|
let Some(hit) = proj.hit_result else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(target_pos) = proj.target_position else { continue };
|
||||||
|
let distance = proj.previous_position.distance(target_pos);
|
||||||
|
|
||||||
|
// расстояние, при котором встраивается шарик
|
||||||
|
const ARRIVAL_DIST: f32 = 5.0;
|
||||||
|
|
||||||
|
if distance > ARRIVAL_DIST {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ищем шарик, в который попали (цель)
|
||||||
|
// собираем данные, чтобы не конфликтовать с мутабельным доступом ниже
|
||||||
|
let mut balls_data: Vec<_> = balls_on_track
|
||||||
|
.iter()
|
||||||
|
.map(|(entity, ball)| (entity, ball.track_progress))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// сортируем по близости к точке удара
|
||||||
|
balls_data.sort_by(|a, b| {
|
||||||
|
let diff_a = (a.1 - hit.hit_progress).abs();
|
||||||
|
let diff_b = (b.1 - hit.hit_progress).abs();
|
||||||
|
diff_a.partial_cmp(&diff_b).unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some((target_entity, original_target_progress)) = balls_data.first() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// копируем значение
|
||||||
|
let original_progress = *original_target_progress;
|
||||||
|
|
||||||
|
// размер сдвига посчитан в треке
|
||||||
|
let shift = track.min_spawn_gap;
|
||||||
|
|
||||||
|
// ставим на место шарика-цели, а сам шарик-цель сдвигаем вперед по треку
|
||||||
|
for (entity, mut ball) in balls_on_track.iter_mut() {
|
||||||
|
if entity == *target_entity {
|
||||||
|
ball.track_progress = (original_progress + shift).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entity, mut ball) in balls_on_track.iter_mut() {
|
||||||
|
ball.track_progress = (ball.track_progress + shift).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// спавним новый шарик на месте цели
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: asset_server.load(proj.ball_type.asset_path()),
|
||||||
|
custom_size: Some(Vec2::splat(ROUND_BALL_SIZE)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform::from_translation(hit.hit_position.extend(ROUND_BALL_Z)),
|
||||||
|
RoundBall {
|
||||||
|
ball_type: proj.ball_type,
|
||||||
|
track_progress: original_progress,
|
||||||
|
},
|
||||||
|
LinearStateMarker,
|
||||||
|
));
|
||||||
|
|
||||||
|
// деспавн шарика-снаряда
|
||||||
|
commands.entity(proj_entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue