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

Уроки · 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 — рамка дойдёт до самого края экрана:

src/main.rs
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 она сливается с настоящим краем экрана.