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

Уроки · 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 на это:

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), а когда целей нет, расставляет поле заново.