Перейти к содержимому
Тема 2 из 5
Теория

Игровой цикл

Как оживает картинка

Мультфильм — это много картинок подряд. Игра делает то же самое: примерно 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.

Шаг мяча

Лёгкая+10 XP

Мяч — объект { x, y, vx, vy }: координаты центра и скорость в пикселях в секунду. Напиши функцию moveBall(ball, dt), которая сдвигает мяч на время dt секунд: x меняется на vx * dt, y — на vy * dt. Функция меняет сам объект ball и ничего не возвращает.

Ниже в заготовке уже есть игровой цикл, который рисует мяч, — нажми «Запустить» и посмотри, как он полетит.

ball = { x: 10, y: 20, vx: 100, vy: -50 }
moveBall(ball, 0.5)  →  ball.x = 60, ball.y = -5

Подключи коннектор, чтобы решать задачи

Код задач выполняется на твоём компьютере — так быстрее и надёжнее, чем в браузере. Для этого нужна бесплатная программа-коннектор CodeWithMe. Подключить её нужно один раз.

Ищем коннектор на этом компьютере…

  1. 1
    Скачай коннектор для своей системы (Windows, macOS или Linux).Скачать коннекторНа странице загрузки выбери файл со своей системой в названии, например windows-amd64.
  2. 2Запусти его и выбери папку, где будут лежать твои решения. Python и Node.js он поставит туда сам, когда понадобится.
  3. 3
    Введи ниже код из 6 цифр — он написан в окне коннектора («Код для сайта CodeWithMe»). Если браузер спросит про доступ к локальной сети — разреши.

Коннектор открытый (исходный код на GitHub), работает только на твоём компьютере и выполняет код лишь после ввода кода сопряжения.