Перейти к содержимому

Уроки · 61

Сектор обнаружения

В уроке «Радиус обнаружения» турель замечала игрока по расстоянию — ближе радиуса или нет. Но бывает нужно обнаружение по направлению: например, игрок должен попасть не в любую точку вокруг, а именно в сектор — клин между двумя лучами, выходящими из одной точки.

Сектор задают два угла: первый луч — начало, второй — конец. Между ними по часовой стрелке и лежит сектор. Если лучи поменять местами, получится другой, дополняющий сектор — та же пара лучей, но проверяется уже другая сторона.

Это мы уже умеем — как в уроке «Угол вектора», и сразу в радианах, без перевода в градусы:

let dx = mouse_x - center_x;
let dy = mouse_y - center_y;
let mouse_angle = dy.atan2(dx);

Напрашивается просто сравнить: sector_start <= mouse_angle && mouse_angle <= sector_end. Работает, пока сектор не переходит через точку, где оборот обрывается и начинается заново. Если для наглядности представить это в градусах: sector_start = 350°, sector_end = 10° — совершенно нормальный сектор в 20°, просто захватывающий ноль. Курсор под углом 5° на картинке явно внутри него. Но в проверке sector_start <= mouse_angle это 350 <= 5 — а это уже само по себе неверно, хотя курсор только что был «внутри». Из-за этого всё && целиком даёт false, и наивная проверка ошибочно говорит «снаружи». (Дальше в коде таких чисел не будет — там всё в радианах, градусы были только для примера.)

Решение уже знакомо — «Угол от 0 до 360°», только применённое не один раз, а дважды. Вместо сравнения двух абсолютных углов меряем, сколько радиан по часовой стрелке от начала сектора — и до курсора, и до конца сектора:

use std::f32::consts::PI;
// Сколько радиан по часовой стрелке от sector_start до sector_end —
// весь сектор целиком.
let sector_span = (sector_end - sector_start).rem_euclid(2.0 * PI);
// Сколько радиан по часовой стрелке от sector_start до курсора.
let relative = (mouse_angle - sector_start).rem_euclid(2.0 * PI);
let inside = relative <= sector_span;

rem_euclid гарантирует, что оба числа лежат в 0..2π, поэтому relative <= sector_span работает одинаково, пересекает сектор точку обрыва оборота или нет — разбирать случаи отдельно не нужно.

Почему это вообще работает: сектор пересекает рубеж ровно тогда, когда sector_end меньше sector_start, — и тогда sector_end - sector_start получается отрицательным. Модуль этого числа — размер не нашего сектора, а дополняющего, того, что остаётся снаружи. В примере выше: 10° - 350° = -340°, а 340° — это и есть дополняющий сектор (360° - 20°). rem_euclid превращает -340° в -340° + 360° = 20° — ровно наш сектор, а не его дополнение.

Сами лучи рисуются обычной draw_line, только достаточно длинной, чтобы выйти за край экрана, — та же идея, что и у пули до выстрела в уроке «Турель стреляет»: не считаем точную длину, просто берём число с запасом.

Два луча сами по себе неоднозначны — непонятно, какая из двух сторон между ними проверяется. Поэтому добавлен третий, короткий жёлтый отрезок — ровно посередине сектора. Он не участвует в проверке и ничего не считает заново, кроме уже посчитанного sector_span, — просто подсказка на глаз, какая сторона имеется в виду.

Серые лучи — границы сектора: от start до end по часовой стрелке, оба в радианах. Жёлтый отрезок покороче — просто ориентир, какая из двух сторон проверяется, на саму проверку не влияет. Води мышью — турель красная, пока курсор внутри сектора, иначе синяя. Подвигай оба поля:

src/main.rs
use macroquad::prelude::*;
use std::f32::consts::PI;
#[macroquad::main("Сектор обнаружения")]
async fn main() {
loop {
clear_background(BLACK);
// Первый луч сектора — начало.
let sector_start = 1.57;
// Второй луч сектора — конец. От первого до второго — по
// часовой стрелке.
let sector_end = 3.14;
let center_x = screen_width() / 2.0;
let center_y = screen_height() / 2.0;
let (mouse_x, mouse_y) = mouse_position();
let dx = mouse_x - center_x;
let dy = mouse_y - center_y;
// Угол вектора направления к курсору — как в уроке «Угол вектора», в радианах.
let mouse_angle = dy.atan2(dx);
// Сколько радиан по часовой стрелке от sector_start до
// sector_end — весь сектор целиком (rem_euclid сам разбирается
// с переходом через точку обрыва оборота, как в уроке «Угол от
// 0 до 360°»).
let sector_span = (sector_end - sector_start).rem_euclid(2.0 * PI);
// Сколько радиан по часовой стрелке от sector_start до курсора.
let relative = (mouse_angle - sector_start).rem_euclid(2.0 * PI);
let inside = relative <= sector_span;
// Граничные лучи сектора — уходят далеко за край экрана.
let ray_length = 2000.0;
let start_x = center_x + sector_start.cos() * ray_length;
let start_y = center_y + sector_start.sin() * ray_length;
draw_line(center_x, center_y, start_x, start_y, 3.0, GRAY);
let end_x = center_x + sector_end.cos() * ray_length;
let end_y = center_y + sector_end.sin() * ray_length;
draw_line(center_x, center_y, end_x, end_y, 3.0, GRAY);
// Биссектриса — ровно посередине сектора, только чтобы видно
// было, какая сторона проверяется. На саму проверку не влияет.
let middle_angle = sector_start + sector_span / 2.0;
let middle_x = center_x + middle_angle.cos() * 120.0;
let middle_y = center_y + middle_angle.sin() * 120.0;
draw_line(center_x, center_y, middle_x, middle_y, 3.0, YELLOW);
// Турель красная, если курсор внутри сектора, иначе синяя.
if inside {
draw_circle(center_x, center_y, 40.0, RED);
} else {
draw_circle(center_x, center_y, 40.0, BLUE);
}
// Игрок — кружок под курсором.
draw_circle(mouse_x, mouse_y, 25.0, WHITE);
next_frame().await;
}
}
  • mouse_angle — угол вектора направления к курсору, в радианах, тем же atan2, что и в уроке «Угол вектора» — без перевода в градусы, прямо то, что он возвращает.
  • sector_span и relative — оба в 0..2π благодаря rem_euclid, поэтому relative <= sector_span верно определяет попадание в сектор при любых sector_start/sector_end, даже если сектор переходит через точку обрыва оборота.
  • Серые лучи — границы сектора, намеренно очень длинные, чтобы выходить за край экрана.
  • Жёлтый отрезок — только для наглядности, какая сторона проверяется; на inside он не влияет.
  • Турель красная, пока курсор внутри сектора, иначе синяя — та же идея, что и в уроке «Радиус обнаружения».