Уроки · 77
Поиск ближайшей цели
Давай сделаем так, чтобы турель сама находила ближайшую цель и поворачивалась к ней. Мишени расставлены случайно, а клик убирает ту, на которую смотрит турель. Тут же она ищет следующую.
Поле целей
Заголовок раздела «Поле целей»Мишени лежат в HashMap: ключ — это ID, а значение — координаты, как
в уроке «HashMap». Расставляет их своя функция
make_targets, поэтому в main для этого остаётся одна строка.
Координаты случайные, а зерно берётся из времени, как в уроке
«Новая случайность при каждом запуске». Поэтому после
перезагрузки страницы поле каждый раз другое.
Ищем ближайшую
Заголовок раздела «Ищем ближайшую»Ближайшую цель находят так: смотрим на мишени по очереди и помним лучшую из тех, что уже видели. Первая мишень сразу становится лучшей. Каждая следующая сравнивается с ней: если она ближе, запоминаем уже её. Когда мишени кончились, в памяти остаётся самая близкая.
Чтобы первая мишень сразу оказалась лучшей, начальное расстояние
ставим самым большим числом, какое умеет хранить f32. Оно
называется f32::MAX, и любое настоящее расстояние меньше него:
let mut best_id = 0; // ID лучшей цели на данный моментlet mut best_distance = f32::MAX; // расстояние до неё, сначала самое большое
for (&id, &(x, y)) in &targets { // Расстояние от турели до этой мишени. let d = distance(center_x, center_y, x, y); // Строго ближе, чем всё, что видели раньше, — запоминаем её. if d < best_distance { best_distance = d; best_id = id; }}Запоминаем только два числа: расстояние до лучшей цели и её ID.
Координаты хранить не нужно: по ID их всегда можно достать из
HashMap.
А что, если две мишени стоят на одинаковом расстоянии от турели? Мы
осматриваем их по очереди и заменяем лучшую, только когда новая
строго ближе. Поэтому ближайшей останется та мишень, которая
попалась первой, а вторая её не вытеснит. Какая из двух попадётся
первой, зависит от порядка, в котором HashMap отдаёт мишени, и этот
порядок мы не выбираем.
А если целей нет
Заголовок раздела «А если целей нет»Если мишеней нет совсем, то и лучшей нет. Чтобы это знать, заведём
found — переменную типа bool из урока «Тип bool».
Сначала в ней false, а как только нашли первую цель, ставим true.
Куда смотреть
Заголовок раздела «Куда смотреть»Турель смотрит на точку. Сначала это точка правее самой турели, и так она смотрит вправо, пока целей нет. А когда цель найдена, берём её координаты и записываем в эту точку:
// Точка, на которую смотрит турель. Если целей нет — точка правее турели.let mut look_x = center_x + 100.0;let mut look_y = center_y;
if found { // Достаём координаты выбранной цели по её ID. let (target_x, target_y) = targets[&best_id]; look_x = target_x; look_y = target_y;}Квадратные скобки с ID работают как индекс у списка: по ID достаём
значение из HashMap. Знак & перед best_id значит «дай ссылку на
это число, а не само число», как в уроке
«Владение и ссылки». Чтобы найти значение, HashMap
нужно только посмотреть на ключ и сравнить его с теми, что лежат
внутри. Забирать ключ себе незачем, поэтому мы передаём ссылку.
Мишень с таким ID должна существовать, поэтому обращаемся к ней только
когда found равно true. Дальше direction считает направление от
турели к этой точке.
Клик убирает ту цель, на которую смотрит турель: targets.remove(&best_id).
Знак & здесь по той же причине, что и выше: HashMap нужно только
посмотреть на ключ. Читать targets мы к этому моменту уже закончили,
поэтому удалять из него можно, как в уроке
«HashMap». На следующем кадре поиск запускается снова, и
турель находит уже другую ближайшую цель. А если целей не осталось,
клик расставляет поле заново.
Замени содержимое src/main.rs на это:
use macroquad::prelude::*;use std::collections::HashMap;
// Радиус мишени.const TARGET_RADIUS: f32 = 40.0;// Сколько мишеней расставляем.const TARGET_COUNT: u32 = 10;
// Своя функция: по двум точкам считает направление от первой ко второй,// как в уроке «Свои функции».fn direction(from_x: f32, from_y: f32, to_x: f32, to_y: f32) -> (f32, f32) { let dx = to_x - from_x; let dy = to_y - from_y; let distance = (dx * dx + dy * dy).sqrt();
if distance > 0.0 { (dx / distance, dy / distance) } else { (1.0, 0.0) }}
// Своя функция: расстояние между двумя точками, как в уроке «Расстояние».fn distance(from_x: f32, from_y: f32, to_x: f32, to_y: f32) -> f32 { let dx = to_x - from_x; let dy = to_y - from_y; (dx * dx + dy * dy).sqrt()}
// Своя функция: расставляет мишени в случайных местах экрана. Ключ — ID// мишени, значение — её координаты.fn make_targets() -> HashMap<u32, (f32, f32)> { // Мишеней пока нет. let mut targets: HashMap<u32, (f32, f32)> = HashMap::new();
for id in 0..TARGET_COUNT { // Случайная точка, отступив от краёв на радиус, чтобы мишень // целиком поместилась на экране. let x = rand::gen_range(TARGET_RADIUS, screen_width() - TARGET_RADIUS); let y = rand::gen_range(TARGET_RADIUS, screen_height() - TARGET_RADIUS); // Кладём мишень в HashMap под её ID. targets.insert(id, (x, y)); }
targets}
#[macroquad::main("Поиск ближайшей цели")]async fn main() { // Зерно — текущее время, поэтому при каждом запуске поле новое. rand::srand(macroquad::miniquad::date::now() as u64);
// Расставляем мишени при старте. let mut targets = make_targets();
loop { clear_background(BLACK);
// Турель стоит в центре экрана. let center_x = screen_width() / 2.0; let center_y = screen_height() / 2.0;
// Ищем ближайшую цель. found — нашли ли хоть одну. let mut found = false; // ID лучшей цели на данный момент. Пока цель не найдена, это // значение никто не читает. let mut best_id = 0; // Расстояние до неё. Сначала — самое большое число f32, чтобы // первая же мишень оказалась ближе. let mut best_distance = f32::MAX;
// Смотрим на каждую мишень по очереди. for (&id, &(x, y)) in &targets { // Расстояние от турели до этой мишени. let d = distance(center_x, center_y, x, y); // Строго ближе, чем всё, что видели раньше, — запоминаем её. if d < best_distance { best_distance = d; best_id = id; found = true; } }
// Точка, на которую смотрит турель. Если целей нет — точка // правее турели. let mut look_x = center_x + 100.0; let mut look_y = center_y; if found { // Достаём координаты выбранной цели по её ID. let (target_x, target_y) = targets[&best_id]; look_x = target_x; look_y = target_y; }
// Направление от турели к этой точке. let (direction_x, direction_y) = direction(center_x, center_y, look_x, look_y);
// Глазик — центр турели плюс направление, растянутое на 25 пикселей. let eye_x = center_x + direction_x * 25.0; let eye_y = center_y + direction_y * 25.0;
// Кнопка мыши только что нажата. if is_mouse_button_pressed(MouseButton::Left) { if found { // Убираем выбранную цель. Читать targets мы уже закончили. targets.remove(&best_id); } else { // Целей не осталось — расставляем поле заново. targets = make_targets(); } }
// Рисуем мишени. for (&id, &(x, y)) in &targets { // Сначала красим мишень в красный. let mut color = RED; // Если это выбранная цель — перекрашиваем в оранжевый. if found && id == best_id { color = ORANGE; } draw_circle(x, y, TARGET_RADIUS, color); }
// Тело турели. draw_circle(center_x, center_y, 40.0, BLUE); // Глазик — смотрит на выбранную цель. draw_circle(eye_x, eye_y, 10.0, WHITE);
next_frame().await; }}Запусти как обычно:
cargo runОранжевая мишень — та, на которую смотрит турель. Кликни, и она пропадёт, а турель повернётся к следующей. Попробуй сам:
Что здесь происходит
Заголовок раздела «Что здесь происходит»make_targets— своя функция: расставляет мишени в случайных местах и отдаёт готовыйHashMap. Её вызывают при старте и когда целей не осталось.rand::srand(...)с текущим временем задаёт зерно, поэтому при каждом запуске поле новое.best_distanceсначала равноf32::MAX,best_idхранит ID лучшей цели на данный момент, аfoundпоказывает, нашли ли хоть одну.- Для каждой мишени считаем расстояние
d, и если оно строго меньшеbest_distance, запоминаем эту мишень вместо прежней. Если две мишени на одинаковом расстоянии, ближайшей остаётся та, которая попалась первой. targets[&best_id]достаёт координаты выбранной цели по её ID. Знак&значит «ссылка на ключ»:HashMapнужно только посмотреть на него, забирать себе незачем.look_xиlook_y— точка, на которую смотрит турель. Сначала это точка правее турели, а если цель найдена, туда записываются её координаты. Направление к этой точке считаетdirection.- Выбранную мишень перекрашиваем в оранжевый, остальные остаются красными.
- Клик убирает выбранную цель через
targets.remove(&best_id), а когда целей нет, расставляет поле заново.