Уроки · 75
Секунды вместо кадров
Если ты открывал урок «Автоматический огонь» на телефоне, то мог заметить, что пули там ползут медленнее, чем на компьютере. На разных устройствах одна и та же программа может выглядеть по-разному. Давай разберёмся, почему так бывает и как сделать, чтобы везде было одинаково.
Почему на разных устройствах по-разному
Заголовок раздела «Почему на разных устройствах по-разному»В предыдущих уроках мы всё считали в кадрах: на каждом кадре пуля сдвигалась на одно и то же число пикселей. Но кадров в секунду у разных устройств получается разное количество.
На компьютере кадр может длиться около 0.016 секунды. Это примерно
60 кадров в секунду. На телефоне кадр может длиться 0.05 секунды,
то есть всего 20 кадров в секунду.
Пусть пуля сдвигается на 10 пикселей за кадр. На компьютере за секунду она пройдёт около 600 пикселей, а на телефоне только 200. Кадров в секунду на компьютере больше, поэтому там пуля и летит быстрее.
Настоящее время кадра
Заголовок раздела «Настоящее время кадра»Чтобы пули везде летели одинаково, нужно знать, сколько времени на
самом деле прошло между прошлым кадром и текущим. Для этого в macroquad
есть get_frame_time(). Она возвращает число секунд, например 0.016
на компьютере или 0.05 на телефоне. Такое число принято называть
dt, от английского delta time, то есть «разница во времени»:
насколько сдвинулись часы между двумя кадрами.
Теперь скорость мы задаём не «пикселей за кадр», а «пикселей в
секунду». А на каждом кадре сдвигаем пулю на скорость, умноженную на
dt:
// dt (delta time) — сколько секунд прошло между прошлым кадром и этим.let dt = get_frame_time();
// Скорость в пикселях в секунду, умноженная на dt, — это шаг за один кадр.bullet.x += bullet.direction_x * SPEED * dt;Возьмём скорость 600 пикселей в секунду. Компьютер за кадр сдвинет
пулю на 600 × 0.016, то есть примерно на 10 пикселей. Телефон сдвинет
на 600 × 0.05, то есть на 30 пикселей. На телефоне кадров меньше, но
каждый шаг больше, поэтому за секунду пуля проходит те же 600
пикселей.
Пауза между выстрелами тоже в секундах
Заголовок раздела «Пауза между выстрелами тоже в секундах»Счётчик перезарядки точно так же считал кадры, а теперь пусть считает
секунды. SHOOT_INTERVAL будет временем между выстрелами, например
0.15 секунды. Счётчик уменьшается на dt, то есть на то время,
которое прошло с прошлого кадра:
// Счётчик уменьшается на столько секунд, сколько прошло с прошлого кадра.cooldown -= dt;
// Кнопка зажата и ждать больше не нужно — стреляем и запускаем отсчёт заново.if is_mouse_button_down(MouseButton::Left) && cooldown <= 0.0 { cooldown = SHOOT_INTERVAL;}Число теперь дробное, и ниже нуля ему можно уходить. «Ноль или меньше» значит то же самое, что и ноль: стрелять можно.
Открой этот урок на телефоне и на компьютере. Теперь пули летят практически одинаково, и турель стреляет практически с одной частотой.
Попробуй сам
Заголовок раздела «Попробуй сам»Зажми левую кнопку мыши на турели и води мышью. Меняй скорость пули и паузу между выстрелами:
use macroquad::prelude::*;use std::collections::HashMap;
// Пауза между выстрелами в секундах. Чем меньше число, тем чаще выстрелы.const SHOOT_INTERVAL: f32 = 0.15;// Скорость пули в пикселях в секунду.const SPEED: f32 = 600.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: f32 = 0.0;
loop { clear_background(BLACK);
// dt (delta time) — сколько секунд прошло между прошлым кадром и этим. let dt = get_frame_time();
// Турель стоит в центре экрана. 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;
// Счётчик уменьшается на столько секунд, сколько прошло с прошлого // кадра. Если он ушёл ниже нуля — ничего страшного, это тоже // значит «можно стрелять». cooldown -= dt;
// Кнопка зажата и ждать больше не нужно — стреляем. if is_mouse_button_down(MouseButton::Left) && cooldown <= 0.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 * dt; bullet.y += bullet.direction_y * SPEED * dt; }
// Удалять во время чтения нельзя, поэтому 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; }}Что здесь происходит
Заголовок раздела «Что здесь происходит»- Кадров в секунду у разных устройств получается разное количество, поэтому движение «на столько-то пикселей за кадр» везде выглядит по-разному.
dt— это delta time, время между прошлым кадром и текущим в секундах. Его возвращаетget_frame_time().SPEEDтеперь в пикселях в секунду, а шаг за кадр равенSPEED * dt. Чем длиннее кадр, тем больше шаг, и за секунду пуля проходит одно и то же расстояние.cooldownсчитается в секундах и уменьшается наdtна каждом кадре. Когда он равен нулю или меньше, можно стрелять.SHOOT_INTERVALзадаёт паузу между выстрелами в секундах: чем меньше число, тем чаще выстрелы.