Уроки · 44
Турель стреляет очередью
В уроке «Турель стреляет» была ровно одна пуля: новый клик не
добавлял вторую, а просто переставлял ту же самую на новое место,
стирая предыдущий выстрел. Теперь, когда мы умеем создавать объекты
прямо в процессе работы программы — через Vec и .push(), как в
уроке «Список кружков» — можно накапливать пули, а не
только держать одну.
Переиспользуем direction
Заголовок раздела «Переиспользуем direction»Внутри 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 разрешает и читать, и
записывать: обновляем позицию каждой пули и сразу рисуем её в новом
месте, одним и тем же циклом.
Пули, улетевшие за край экрана, никуда не деваются — список продолжает расти, сколько бы ни было кликов. Это не забытая деталь, а осознанное упрощение: с этим разберёмся в одном из следующих уроков.
Попробуй сам
Заголовок раздела «Попробуй сам»Подвигай мышь — глазик поворачивается вслед за ней. Кликай сколько угодно раз — каждый клик добавляет новую пулю, и все они летят одновременно, каждая своим направлением:
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, а не только читает их.- Направление у каждой пули своё и зафиксировано в момент выстрела — та же идея, что и в уроке «Турель стреляет»: пуля летит по прямой, не подстраиваясь под мышь.
- Список пуль ничем не ограничен и никогда не уменьшается — сколько кликов, столько и пуль в списке, даже если их давно не видно на экране.