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

Уроки · 74

Автоматический огонь

Давай сделаем так, чтобы турель стреляла сама, пока зажата кнопка мыши. В уроке «Стрельба по мишеням» на каждую пулю нужен был отдельный клик, а теперь будет достаточно просто держать кнопку.

Раньше выстрел происходил по is_mouse_button_pressed. Она верна только на одном кадре, в тот момент, когда кнопку нажали. Нам нужна is_mouse_button_down из урока «Клик мышью»: она верна на каждом кадре, пока кнопка зажата.

Но если просто заменить одну функцию на другую, турель будет выпускать по пуле на каждом кадре, и получится сплошная струя. Между выстрелами нужна пауза.

Заведём число cooldown, то есть сколько кадров осталось подождать до следующего выстрела. Каждый кадр оно уменьшается на единицу, пока не дойдёт до нуля. Ноль значит «можно стрелять». После выстрела мы снова кладём в счётчик SHOOT_INTERVAL, и отсчёт начинается заново:

// Счётчик идёт к нулю и ниже нуля не опускается.
if cooldown > 0 {
cooldown -= 1;
}
// Кнопка зажата и ждать больше не нужно — стреляем и запускаем отсчёт заново.
if is_mouse_button_down(MouseButton::Left) && cooldown == 0 {
cooldown = SHOOT_INTERVAL;
}

Ниже нуля счётчик опускаться не должен: он считает, сколько кадров осталось ждать, а ждать «минус три кадра» нельзя. Поэтому единицу мы вычитаем, только пока cooldown больше нуля.

В начале cooldown равен нулю, поэтому первый выстрел происходит сразу, как только ты зажал кнопку. Отпустишь кнопку, и счётчик всё равно дойдёт до нуля сам, так что следующее нажатие выстрелит сразу, если пауза уже закончилась.

Всё остальное нам уже знакомо. Пули лежат в HashMap со своими ID, как в уроке «Стрельба по мишеням», и пропадают за краем экрана тем же способом.

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

src/main.rs
use macroquad::prelude::*;
use std::collections::HashMap;
// Через сколько кадров после выстрела можно стрелять снова.
// Чем меньше число, тем чаще выстрелы.
const SHOOT_INTERVAL: u32 = 10;
// Скорость пули — одна и та же на каждом кадре, поэтому это константа
// (как в уроке «Константы»).
const SPEED: f32 = 10.0;
// Радиус пули.
const BULLET_RADIUS: f32 = 8.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() {
// Пули лежат в HashMap, у каждой свой ID — как в уроке «Стрельба по мишеням».
let mut bullets: HashMap<u32, Bullet> = HashMap::new();
// ID, который получит следующая пуля.
let mut next_id: u32 = 0;
// Сколько кадров осталось ждать до следующего выстрела. 0 — можно стрелять.
let mut cooldown: u32 = 0;
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 cooldown > 0 {
cooldown -= 1;
}
// Кнопка зажата и ждать больше не нужно — стреляем.
if is_mouse_button_down(MouseButton::Left) && cooldown == 0 {
// Новая пуля из глазика под своим ID.
bullets.insert(
next_id,
Bullet { x: eye_x, y: eye_y, direction_x, direction_y },
);
// Следующая пуля получит следующий ID.
next_id += 1;
// Запускаем отсчёт заново.
cooldown = SHOOT_INTERVAL;
}
// Двигаем каждую пулю её собственным направлением.
for bullet in bullets.values_mut() {
bullet.x += bullet.direction_x * SPEED;
bullet.y += bullet.direction_y * SPEED;
}
// Удалять во время чтения нельзя, поэтому ID пуль, которые
// вылетели за край, сначала собираем в список.
let mut bullets_to_remove: Vec<u32> = Vec::new();
for (&bullet_id, bullet) in &bullets {
// Сам экран и есть прямоугольник: от (0, 0) до (screen_width(),
// screen_height()). Пуля снаружи — на удаление.
if !inside_rectangle(bullet.x, bullet.y, 0.0, 0.0, screen_width(), screen_height()) {
bullets_to_remove.push(bullet_id);
}
}
// Чтение закончилось — теперь можно убирать.
for id in bullets_to_remove {
bullets.remove(&id);
}
// Тело турели.
draw_circle(center_x, center_y, 40.0, BLUE);
// Глазик — смотрит на мышь.
draw_circle(eye_x, eye_y, 10.0, WHITE);
// Пули.
for bullet in bullets.values() {
draw_circle(bullet.x, bullet.y, BULLET_RADIUS, YELLOW);
}
next_frame().await;
}
}
  • is_mouse_button_down верна на каждом кадре, пока кнопка зажата. А is_mouse_button_pressed срабатывает один раз, в момент нажатия.
  • cooldown показывает, сколько кадров осталось ждать. Объявлен он до loop, чтобы помнить своё значение от кадра к кадру.
  • if cooldown > 0 { cooldown -= 1; } уменьшает счётчик до нуля и останавливает его там.
  • && cooldown == 0 разрешает выстрел только тогда, когда счётчик дошёл до нуля.
  • cooldown = SHOOT_INTERVAL запускает отсчёт заново сразу после выстрела.
  • SHOOT_INTERVAL задаёт паузу в кадрах: чем меньше число, тем чаще выстрелы. При 1 турель стреляет на каждом кадре.
  • bullets — это HashMap<u32, Bullet>, а next_id выдаёт каждой новой пуле свой ID. Пули, вылетевшие за край, попадают в bullets_to_remove и удаляются после того, как мы закончили читать bullets.