Игровой цикл
Как оживает картинка
Мультфильм — это много картинок подряд. Игра делает то же самое: примерно 60 раз в секунду стирает холст и рисует всё в новом положении. Повторяющийся кусок кода, который это делает, называют игровым циклом.
requestAnimationFrame
Просить браузер вызвать функцию перед следующей перерисовкой экрана — requestAnimationFrame:
function frame() {
// 1. изменить положение объектов
// 2. нарисовать кадр
requestAnimationFrame(frame); // и попросить следующий кадр
}
requestAnimationFrame(frame);
Почему не setInterval(frame, 16)? requestAnimationFrame подстраивается под частоту экрана
(60, 120, 144 Гц), не рисует впустую, когда вкладка скрыта, и кадры получаются плавными, без
рывков. Это стандарт для анимации в браузере.
Нельзя делать игровой цикл через while (true) { ... }: такой цикл никогда не отдаёт управление
браузеру, страница зависнет и ничего не нарисует.
Время кадра: dt
Кадры идут неравномерно: на быстром компьютере их 144 в секунду, на медленном — 30. Если двигать
мяч «на 2 пикселя за кадр», на разных компьютерах он полетит с разной скоростью. Поэтому скорость
задают в пикселях в секунду, а в каждом кадре двигают на скорость × dt, где dt — сколько
секунд прошло с прошлого кадра.
requestAnimationFrame передаёт в функцию текущее время в миллисекундах:
let last = performance.now();
function frame(now) {
const dt = (now - last) / 1000; // миллисекунды → секунды
last = now;
ball.x += ball.vx * dt; // vx — пикселей в секунду
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
Если мяч летит со скоростью 120 px/с, за кадр в 1/60 секунды он сдвинется на 2 пикселя, а за кадр в 1/30 — на 4. За секунду в обоих случаях — ровно 120.
Когда вкладку сворачивают, кадры останавливаются, а при возврате dt получается огромным (минуты!)
— объекты «телепортируются» сквозь стены. Поэтому dt обычно ограничивают: Math.min(dt, 0.1).
Состояние, update и draw
Всё, что описывает игру в данный момент — позиции, скорости, счёт, жизни, — хранят в объектах состояния. Кадр делят на две части:
const state = {
ball: { x: 50, y: 50, vx: 150, vy: 90, r: 10 },
score: 0,
};
function update(dt) { // только меняет числа
state.ball.x += state.ball.vx * dt;
state.ball.y += state.ball.vy * dt;
}
function draw() { // только рисует то, что в состоянии
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(state.ball.x, state.ball.y, state.ball.r, 0, Math.PI * 2);
ctx.fill();
ctx.fillText(`Счёт: ${state.score}`, 10, 20);
}
let last = performance.now();
function frame(now) {
const dt = Math.min(0.1, (now - last) / 1000);
last = now;
update(dt);
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
Зачем делить? update можно проверить без экрана — просто вызвать с числами и посмотреть
результат (так и устроены задачи курса). А draw не думает о правилах игры, он только показывает
состояние. Это та же идея, что «данные → render» в DOM.
Ускорение и гравитация
Скорость тоже может меняться со временем. Гравитация — это постоянное ускорение вниз: каждую секунду скорость падения растёт на одно и то же число.
const GRAVITY = 300; // px/с за секунду
ball.vy += GRAVITY * dt; // сначала меняем скорость
ball.y += ball.vy * dt; // потом двигаемся с новой скоростью
Так получаются прыжки, падения и красивые параболы у искр фейерверка.
Объекты появляются и исчезают
Пули, искры, враги живут недолго. Их держат в массиве: новые добавляют через push, а лишние
убирают, оставляя только живых:
bullets = bullets.filter((b) => b.y > 0); // вылетевшие за экран — удалить
Итог
- Игровой цикл:
requestAnimationFrame(frame)в конце каждого кадра; никакихwhile (true). - Скорость — в пикселях в секунду, сдвиг за кадр —
скорость × dt,dtв секундах и не больше 0.1. - Состояние игры — в объектах;
update(dt)меняет числа,draw()рисует. - Гравитация:
vy += g × dt, затемy += vy × dt. Временные объекты — в массиве +filter.