Уроки · 47
Пули пропадают за краем
В уроке «Турель стреляет очередью» список пуль рос без остановки — это было осознанным упрощением. Пора его убрать: пули, улетевшие достаточно далеко, должны пропадать из списка.
Видимая граница вместо настоящего края экрана
Заголовок раздела «Видимая граница вместо настоящего края экрана»Если убирать пули ровно по краю экрана, это будет незаметно — пуля и так пропадала бы из вида, покидая экран. Чтобы увидеть сам момент исчезновения, возьмём границу заметно меньше экрана.
margin — это отступ каждой стороны рамки внутрь, от края экрана.
Левый верхний угол экрана — это точка (0, 0), поэтому сдвинутый на
margin внутрь угол рамки — это просто (margin, margin):
// Левый верхний угол рамки — сдвинут от (0, 0) вглубь экрана на margin.let view_x = margin;let view_y = margin;С шириной и высотой рамки чуть сложнее: отступ срезает место сразу с
двух противоположных сторон — слева и справа для ширины, сверху и
снизу для высоты. Поэтому margin вычитается из размера экрана
дважды:
// Минус margin с каждой из двух противоположных сторон.let view_width = screen_width() - margin * 2.0;let view_height = screen_height() - margin * 2.0;Эти четыре числа и задают рамку, которую рисуем контуром:
// Рисуем только контур рамки, чтобы не закрывать пули и турель.draw_rectangle_lines(view_x, view_y, view_width, view_height, 8.0, LIGHTGRAY);draw_rectangle_lines — то же самое, что draw_circle_lines в уроке
«Расстояние», только контур не круга, а
прямоугольника. Демка ниже даёт настроить margin: сделай его
нулевым — и рамка дойдёт до настоящего края экрана.
Проверка попадания — своей функцией
Заголовок раздела «Проверка попадания — своей функцией»Считать, внутри ли точка прямоугольника, мы уже умеем — четыре
условия через &&, как в уроке «Прямоугольные
кнопки». На этот раз вынесем это в отдельную
функцию, раз проверять предстоит для каждой пули на каждом кадре:
fn inside_rectangle(x: f32, y: f32, rect_x: f32, rect_y: f32, rect_width: f32, rect_height: f32) -> bool { x >= rect_x && x <= rect_x + rect_width && y >= rect_y && y <= rect_y + rect_height}Параметров тут аж шесть, но пугаться нечего — это просто «точка» и «прямоугольник» порознь, каждый разложен на свои числа:
x,y— проверяемая точка (у нас это будут координаты пули).rect_x,rect_y— левый верхний угол прямоугольника (той самой рамки).rect_width,rect_height— ширина и высота прямоугольника.
Точка — это два числа, прямоугольник — ещё четыре (как в уроке «Прямоугольник»), вместе — ровно шесть параметров.
Забираем список в собственность
Заголовок раздела «Забираем список в собственность»Это тот же приём, что и в уроке «Кружки исчезают»:
новый пустой список kept_bullets, for mut bullet in bullets без
& — забирает старый список в собственность целиком, — и для каждой
пули решаем, переносить её в новый список или нет. Разница только в
самом условии: там кружок убирался по клику, здесь пуля остаётся,
только если она ещё внутри рамки:
// Новый пустой список — создаётся заново на каждом кадре, специально// для пуль, которые стоит оставить.let mut kept_bullets: Vec<Bullet> = Vec::new();
// Забираем старый список bullets в собственность — второй раз им// воспользоваться уже не выйдет.for mut bullet in bullets { // Двигаем пулю в её направлении. bullet.x += bullet.direction_x * SPEED; bullet.y += bullet.direction_y * SPEED;
// Пуля ещё внутри рамки — рисуем и переносим её в новый список. if inside_rectangle(bullet.x, bullet.y, view_x, view_y, view_width, view_height) { draw_circle(bullet.x, bullet.y, 8.0, YELLOW); kept_bullets.push(bullet); } // Иначе пуля просто никуда не переносится и пропадает вместе со // старым списком, когда цикл закончится.}
// На следующий кадр bullets станет тем, что мы только что собрали.bullets = kept_bullets;Обрати внимание на for mut bullet in bullets — здесь mut стоит
перед bullet, а не перед bullets. В for bullet in &mut bullets
из прошлого урока bullet был ссылкой на пулю внутри списка, и
эта ссылка уже разрешала менять bullet.x/bullet.y. Здесь же
bullet — не ссылка, а сама пуля целиком, её полноценный владелец, и,
чтобы менять её поля, переменная bullet должна быть объявлена
мутабельной — той же самой mut, что и в уроке «Изменяемые
переменные».
Попробуй сам
Заголовок раздела «Попробуй сам»Кликай сколько угодно раз — пули летят и пропадают, едва пересекая
серую рамку. Сделай margin равным 0 — рамка дойдёт до
самого края экрана:
use macroquad::prelude::*;
// Скорость одна и та же на каждом кадре, поэтому это константа// (как в уроке «Константы»), а не переменная внутри loop.const SPEED: f32 = 10.0;// Отступ границы обзора от краёв экрана.const MARGIN: f32 = 50.0;
// Одна пуля: положение и зафиксированное направление полёта.struct Bullet { x: f32, y: f32, direction_x: f32, direction_y: f32,}
// Своя функция: по двум точкам считает направление от первой ко второй,// как в уроке «Свои функции».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) }}
// Своя функция: точка (x, y) внутри прямоугольника (rect_x, rect_y,// rect_width, rect_height)? Та же проверка, что и в уроке «Прямоугольные// кнопки», только вынесенная отдельно.fn inside_rectangle(x: f32, y: f32, rect_x: f32, rect_y: f32, rect_width: f32, rect_height: f32) -> bool { x >= rect_x && x <= rect_x + rect_width && y >= rect_y && y <= rect_y + rect_height}
#[macroquad::main("Пули пропадают за краем")]async fn main() { // Список пуль — сначала пустой. let mut bullets: Vec<Bullet> = Vec::new();
loop { clear_background(BLACK);
// Турель стоит в центре экрана. let center_x = screen_width() / 2.0; let center_y = screen_height() / 2.0;
// Координаты мыши. let (mouse_x, mouse_y) = mouse_position();
// Куда смотрит турель — направление от центра к мыши. let (direction_x, direction_y) = direction(center_x, center_y, mouse_x, mouse_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) { bullets.push(Bullet { x: eye_x, y: eye_y, direction_x, direction_y, }); }
// Левый верхний угол рамки — сдвинут от (0, 0) вглубь экрана на MARGIN. let view_x = MARGIN; let view_y = MARGIN; // Минус MARGIN с каждой из двух противоположных сторон. let view_width = screen_width() - MARGIN * 2.0; let view_height = screen_height() - MARGIN * 2.0; // Рисуем только контур рамки, чтобы не закрывать пули и турель. draw_rectangle_lines(view_x, view_y, view_width, view_height, 8.0, LIGHTGRAY);
// Тело турели. draw_circle(center_x, center_y, 40.0, BLUE); // Глазик — смотрит на мышь. draw_circle(eye_x, eye_y, 10.0, WHITE);
// Новый пустой список — создаётся заново на каждом кадре, специально // для пуль, которые стоит оставить. let mut kept_bullets: Vec<Bullet> = Vec::new(); // Забираем старый список bullets в собственность — второй раз им // воспользоваться уже не выйдет. for mut bullet in bullets { // Двигаем пулю в её направлении. bullet.x += bullet.direction_x * SPEED; bullet.y += bullet.direction_y * SPEED;
// Пуля ещё внутри рамки — рисуем и переносим её в новый список. if inside_rectangle(bullet.x, bullet.y, view_x, view_y, view_width, view_height) { draw_circle(bullet.x, bullet.y, 8.0, YELLOW); kept_bullets.push(bullet); } // Иначе пуля просто никуда не переносится и пропадает вместе // со старым списком, когда цикл закончится. } // На следующий кадр bullets станет тем, что мы только что собрали. bullets = kept_bullets;
next_frame().await; }}Что здесь происходит
Заголовок раздела «Что здесь происходит»draw_rectangle_linesрисует только контур границы обзора — сам прямоугольник не закрашен, чтобы не закрывать пули и турель.inside_rectangle(...)— своя функция, проверяющая попадание точки в прямоугольник; та же логика, что и в уроке «Прямоугольные кнопки», просто теперь она переиспользуется, а не прописана прямо вloop.for mut bullet in bulletsзабирает список в собственность: старая переменнаяbulletsпосле цикла недоступна, поэтому в конце ей присваивается новое значение —kept_bullets.- Пуля добавляется в
kept_bullets, только если прошла проверкуinside_rectangle— не прошедшие никуда не попадают и пропадают вместе со старым списком. margin, заданный полем ниже, определяет отступ границы от краёв экрана — на0она сливается с настоящим краем экрана.