Уроки · 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 по часовой стрелке, оба в радианах. Жёлтый
отрезок покороче — просто ориентир, какая из двух сторон
проверяется, на саму проверку не влияет. Води мышью — турель
красная, пока курсор внутри сектора, иначе синяя. Подвигай оба
поля:
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он не влияет. - Турель красная, пока курсор внутри сектора, иначе синяя — та же идея, что и в уроке «Радиус обнаружения».