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

Уроки · 44

Турель стреляет очередью

В уроке «Турель стреляет» была ровно одна пуля: новый клик не добавлял вторую, а просто переставлял ту же самую на новое место, стирая предыдущий выстрел. Теперь, когда мы умеем создавать объекты прямо в процессе работы программы — через Vec и .push(), как в уроке «Список кружков» — можно накапливать пули, а не только держать одну.

Внутри loop турели всегда был кусок с dx, dy и sqrt, чтобы посчитать направление от центра к мыши. Мы уже выносили этот же самый расчёт в отдельную функцию direction в уроке «Свои функции» — пересчитывать его заново тут незачем, просто вызываем готовую функцию:

let (direction_x, direction_y) = direction(center_x, center_y, mouse_x, mouse_y);

Так в loop не остаётся лишней математики, и весь урок можно посвятить тому, что здесь действительно новое, — списку пуль.

Раньше положение и направление пули жили в четырёх отдельных переменных: bullet_x, bullet_y, bullet_direction_x, bullet_direction_y. Соберём их в одну структуру — та же идея, что и Circle в уроке «Свои структуры»:

struct Bullet {
x: f32,
y: f32,
direction_x: f32,
direction_y: f32,
}

Список пуль — Vec<Bullet>, сначала пустой:

let mut bullets: Vec<Bullet> = Vec::new();

По клику вместо того, чтобы переставлять одну пулю, добавляем в конец списка новую:

bullets.push(Bullet {
x: eye_x,
y: eye_y,
direction_x,
direction_y,
});

x: eye_x и y: eye_y написаны полностью, потому что имя переменной не совпадает с именем поля. А direction_x и direction_y — тем же сокращением, что и radius в уроке «Свои структуры»: имя переменной и так совпадает с именем поля, писать его дважды незачем. Сокращать можно только совпадающие поля — несовпадающие, как x/y здесь, по-прежнему пишутся полностью, и оба способа спокойно уживаются в одном и том же значении.

В уроке «Список кружков» цикл for &(x, y) in &points только читал точки, чтобы их нарисовать, — сами точки никогда не менялись. Здесь по-другому: x и y каждой пули должны меняться на каждом кадре, ведь пуля летит. Для этого нужен &mut bullets вместо &bullets:

for bullet in &mut bullets {
bullet.x += bullet.direction_x * SPEED;
bullet.y += bullet.direction_y * SPEED;
draw_circle(bullet.x, bullet.y, 8.0, YELLOW);
}

&bullets дал бы доступ только для чтения — менять bullet.x внутри такого цикла не вышло бы. &mut bullets разрешает и читать, и записывать: обновляем позицию каждой пули и сразу рисуем её в новом месте, одним и тем же циклом.

Пули, улетевшие за край экрана, никуда не деваются — список продолжает расти, сколько бы ни было кликов. Это не забытая деталь, а осознанное упрощение: с этим разберёмся в одном из следующих уроков.

Подвигай мышь — глазик поворачивается вслед за ней. Кликай сколько угодно раз — каждый клик добавляет новую пулю, и все они летят одновременно, каждая своим направлением:

src/main.rs
use macroquad::prelude::*;
// Скорость одна и та же на каждом кадре, поэтому это константа
// (как в уроке «Константы»), а не переменная внутри loop.
const SPEED: f32 = 10.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)
}
}
#[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,
});
}
// Тело турели.
draw_circle(center_x, center_y, 40.0, BLUE);
// Глазик — смотрит на мышь.
draw_circle(eye_x, eye_y, 10.0, WHITE);
// Каждая пуля летит своим направлением, независимо от остальных.
for bullet in &mut bullets {
bullet.x += bullet.direction_x * SPEED;
bullet.y += bullet.direction_y * SPEED;
draw_circle(bullet.x, bullet.y, 8.0, YELLOW);
}
next_frame().await;
}
}
  • direction(center_x, center_y, mouse_x, mouse_y) — та же функция, что и в уроке «Свои функции», просто вызванная здесь: избавляет loop от повторного расчёта dx, dy и sqrt.
  • struct Bullet { x, y, direction_x, direction_y } — то, что раньше было четырьмя отдельными переменными, теперь одна структура, как Circle.
  • Vec<Bullet> — список пуль, который растёт по одной с каждым кликом, та же идея, что и Vec<(f32, f32)> в уроке «Список кружков».
  • bullets.push(Bullet { ... }) добавляет новую пулю в конец списка, не трогая уже существующие — в отличие от урока «Турель стреляет», где клик просто переписывал единственную пулю.
  • for bullet in &mut bullets — мутабельная ссылка нужна, потому что тело цикла меняет bullet.x и bullet.y, а не только читает их.
  • Направление у каждой пули своё и зафиксировано в момент выстрела — та же идея, что и в уроке «Турель стреляет»: пуля летит по прямой, не подстраиваясь под мышь.
  • Список пуль ничем не ограничен и никогда не уменьшается — сколько кликов, столько и пуль в списке, даже если их давно не видно на экране.