Игровой цикл (Game Loop) и физика столкновений (AABB, хитбоксы и триггеры) простыми словами и на уровне профи

Коротко

Игровой цикл (Game Loop) — это повторяющаяся последовательность «прочитать ввод → обновить мир → нарисовать кадр», на которую при частоте 60 к/с отводится 16,67 мс, а при 30 к/с — 33,33 мс. Чтобы физика вела себя одинаково на любом компьютере, её обновляют фиксированными шагами через аккумулятор времени, а движение умножают на шаг времени. Столкновения прямоугольников без поворота (AABB — Axis-Aligned Bounding Box) проверяются четырьмя сравнениями и разрешаются выталкиванием по оси наименьшего перекрытия. В статье — формулы, готовый псевдокод, разница между коллайдером, триггером и хитбоксом и проверенный в Node.js код приземления и сбора монеты.

Содержание

  1. Три фазы игрового цикла и бюджет кадра в миллисекундах
  2. deltaTime: почему без него игра работает быстрее на мониторах с высокой частотой обновления
  3. Фиксированный шаг физики и аккумулятор времени
  4. AABB: проверка пересечения двух прямоугольников
  5. Разрешение столкновения: выталкивание по оси X, затем по оси Y
  6. Коллайдер, триггер, хитбокс и хёртбокс: чем они отличаются
  7. Туннелирование: почему быстрые объекты проходят сквозь стены
  8. Практический сценарий: проверяем приземление и сбор монеты в коде на JavaScript
  9. Когда вам это не подойдёт

Три фазы игрового цикла и бюджет кадра в миллисекундах

Игровой цикл каждые 16,67 мс (при 60 к/с) выполняет три фазы: чтение ввода, обновление состояния и отрисовку. Бюджет кадра считается как обратная величина частоты:

t_кадра = 1000 мс / FPS
60 к/с → 16,67 мс
30 к/с → 33,33 мс

Для школьника цикл похож на мультфильм в блокноте: каждая страница — один кадр, и между страницами программа успевает посмотреть, какие кнопки нажаты, сдвинуть героя и нарисовать новую страницу. Если обновление и отрисовка вместе занимают больше бюджета, кадр опаздывает, и игрок видит рывок.

В браузере цикл обычно привязан к функции requestAnimationFrame, которую браузер вызывает перед каждой перерисовкой экрана; её частота совпадает с частотой обновления дисплея — 60 Гц на большинстве мониторов, 120 Гц и выше на игровых (источник: спецификация HTML Living Standard, раздел «Animation frames», и документация MDN window.requestAnimationFrame, проверено: октябрь 2026).

deltaTime: почему без него игра работает быстрее на мониторах с высокой частотой обновления

deltaTime — это время в секундах между текущим и предыдущим кадром; умножение скорости на него делает движение независимым от частоты кадров. Если сдвигать персонажа на 5 пикселей каждый кадр, то при 60 к/с он пройдёт 300 пикселей за секунду, при 30 к/с — 150, а на мониторе 144 Гц — 720. С deltaTime скорость задаётся в пикселях в секунду:

x_новый = x + v * dt
при v = 300 пикс/с: 60 к/с → 5 пикс/кадр, 30 к/с → 10 пикс/кадр

За секунду в обоих случаях получается ровно 300 пикселей.

Фиксированный шаг физики и аккумулятор времени

Фиксированный шаг означает, что физика всегда обновляется порциями одинаковой длины (например, dt_физ = 1/60 с), даже если кадры приходят неравномерно. Переменный шаг даёт разную высоту прыжка на разных компьютерах из-за ошибки численного интегрирования, а при подвисании вкладки браузера один огромный шаг может «телепортировать» героя сквозь пол.

Схема с аккумулятором описана Гленном Фидлером в статье «Fix Your Timestep!» (gafferongames.com, 2004 год, проверено: октябрь 2026):

const STEP = 1 / 60;          // 16,67 мс
const MAX_FRAME = 0.25;       // защита от «спирали смерти»
let accumulator = 0;

function frame(now) {
  let dt = (now - last) / 1000;
  last = now;
  dt = Math.min(dt, MAX_FRAME);
  accumulator += dt;

  readInput();
  while (accumulator >= STEP) {
    physicsStep(STEP);        // всегда одинаковый шаг
    accumulator -= STEP;
  }
  const alpha = accumulator / STEP; // доля до следующего шага
  render(alpha);              // интерполяция позиций для плавности
  requestAnimationFrame(frame);
}

Ограничение MAX_FRAME = 0,25 с нужно, чтобы после возвращения на вкладку игра не пыталась догнать пропущенные секунды сотнями физических шагов подряд.

AABB: проверка пересечения двух прямоугольников

Два прямоугольника, стороны которых параллельны осям, пересекаются тогда и только тогда, когда их проекции перекрываются одновременно по оси X и по оси Y. Если у прямоугольника A левый верхний угол (A_x, A_y), ширина A_w и высота A_h, то условие записывается четырьмя сравнениями:

A_x < B_x + B_w  И  A_x + A_w > B_x  И  A_y < B_y + B_h  И  A_y + A_h > B_y

Строгие знаки < и > означают, что прямоугольники, которые только касаются гранью, не считаются пересекающимися — так персонаж, стоящий точно на полу, не «застревает» в нём.

Проверочный пример: персонаж A = (100, 200, 32, 48) и монета B = (120, 230, 16, 16). Проверяем: 100 < 136, 132 > 120, 200 < 246, 248 > 230 — все четыре условия истинны, монета собрана.

Разрешение столкновения: выталкивание по оси X, затем по оси Y

Разрешение столкновения — это сдвиг объекта из стены на минимальное расстояние по той оси, где перекрытие меньше. Глубина перекрытия по осям считается через центры (cx, cy) и половины размеров (hx, hy):

px = (hx_A + hx_B) - abs(cx_A - cx_B)
py = (hy_A + hy_B) - abs(cy_A - cy_B)

Если px < py, объект выталкивается по X на px в сторону от центра стены, иначе — по Y на py. В платформерах надёжнее двигать персонажа по осям раздельно: сначала сместить по X и сразу разрешить столкновения по X, затем сместить по Y и разрешить по Y. Так персонаж, бегущий вдоль пола, не цепляется за стыки соседних блоков 32×32, а при приземлении обнуляется только вертикальная скорость.

Коллайдер, триггер, хитбокс и хёртбокс: чем они отличаются

Коллайдер физически не пускает объект внутрь, а триггер только сообщает о пересечении и пропускает объект насквозь.

Типы зон столкновения в 2D-игре
Тип зоныФизически выталкивает?Что делает при пересеченииПример на сцене
Твёрдый коллайдерДаОстанавливает движение, обнуляет скорость по оси удараплатформа, стена, пол
ТриггерНетЗапускает событие один раз при входе или каждый кадр внутри зонымонета, ключ, флаг, зона падения
Хитбокс (hitbox)НетНаносит урон тому, чей хёртбокс он пересёкудар мечом, пуля, шипы
Хёртбокс (hurtbox)НетПолучает уронтело персонажа или врага

Хорошее правило честности: хёртбокс персонажа делают на 2–4 пикселя меньше видимого спрайта, а хитбокс опасности — не больше видимой формы, чтобы игрок не погибал от касания пустого воздуха.

Туннелирование: почему быстрые объекты проходят сквозь стены

Туннелирование происходит, когда за один физический шаг объект смещается дальше, чем суммарная толщина стены и его собственный размер. Пуля со скоростью 1 800 пикс/с при шаге 1/60 с проходит 30 пикселей за шаг; стену толщиной 16 пикселей при размере пули 8 пикселей она перепрыгнет, если окажется перед ней в одном шаге и за ней в следующем. Безопасное условие:

v * dt < w_стены + w_объекта
v_max < (16 + 8) * 60 = 1 440 пикс/с

Если скорость нужна выше, шаг дробят на подшаги или проверяют пересечение отрезка движения с прямоугольником стены (swept AABB).

Практический сценарий: проверяем приземление и сбор монеты в коде на JavaScript

Персонаж, падающий на платформу с высоты 128 пикселей, должен встать точно на её верхнюю грань без проваливания и собрать монету, лежащую на 16 пикселей выше платформы; это проверяется прогоном фиксированных шагов без графики. Код ниже проверен в Node.js 24; комментарии показывают его фактический вывод.

// Прямоугольник: левый верхний угол (x, y), ширина w, высота h; ось Y направлена вниз
function overlaps(a, b) {
  return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}

const STEP = 1 / 60, GRAVITY = 1800, MAX_FALL = 720;
const platform = { x: 0, y: 480, w: 640, h: 32 };                // твёрдый коллайдер
const coin = { x: 100, y: 448, w: 16, h: 16, active: true };      // триггер на 16 пикс выше платформы
const player = { x: 100, y: 304, w: 32, h: 48, vy: 0, grounded: false, score: 0 }; // низ на 352: 128 пикс над платформой

function physicsStep(p) {
  // Ось Y: сначала сдвигаем, потом выталкиваем
  p.vy = Math.min(MAX_FALL, p.vy + GRAVITY * STEP);
  p.y += p.vy * STEP;
  p.grounded = false;
  if (overlaps(p, platform) && p.vy > 0) {
    p.y = platform.y - p.h;   // ставим низ персонажа ровно на верхнюю грань
    p.vy = 0;
    p.grounded = true;
  }
  // Триггер: только сообщает о пересечении и не выталкивает
  if (coin.active && overlaps(p, coin)) { coin.active = false; p.score += 1; }
}

let landedAt = -1;
for (let i = 0; i < 120; i++) {                  // 2 секунды фиксированными шагами
  physicsStep(player);
  if (player.grounded && landedAt < 0) landedAt = i + 1;
}
console.log("приземлился на шаге", landedAt);                    // 23
console.log("низ персонажа:", player.y + player.h, "vy:", player.vy); // 480 0
console.log("очки:", player.score);                              // 1

Скорость падения ограничена 720 пикс/с, то есть 12 пикселями за шаг 1/60 с, — это меньше суммы толщины платформы (32) и высоты персонажа (48), поэтому туннелирования нет. Монета — триггер: проверка overlaps только меняет счёт и не трогает координаты персонажа.

Когда вам это не подойдёт

AABB и раздельное выталкивание по осям не подходят для повёрнутых и круглых тел и для сложной физики.

  • Вращающиеся объекты и наклонные поверхности: ящики, которые кувыркаются, и склоны под произвольным углом требуют проверки ориентированных прямоугольников (OBB) по теореме о разделяющей оси или полигональной физики; AABB вокруг повёрнутого объекта даст «пустые» углы.
  • Сотни одновременно движущихся тел без пространственного разбиения: прямой перебор всех пар растёт как N² — 1 000 объектов дают почти 500 000 проверок за шаг.
  • Стопки, шарниры и цепи: ящики, которые давят друг на друга, маятники и верёвки требуют решателя ограничений; ручное выталкивание по осям даст дрожание стопки.

Мы разрабатываем J2DRU — браузерный 2D игровой движок; он в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).

Читайте также:

Частые вопросы

Что такое deltaTime и зачем на него умножать скорость?

deltaTime — это время в секундах, прошедшее с прошлого кадра (около 0,0167 с при 60 к/с и 0,0333 с при 30 к/с). Если умножать скорость в пикселях в секунду на deltaTime, персонаж проходит одинаковое расстояние за секунду при любой частоте кадров.

Чем хитбокс отличается от коллайдера и триггера?

Коллайдер — твёрдая форма, которую физика не даёт пересечь (стена, пол). Триггер — форма, пересечение которой только запускает событие (монета, зона финиша). Хитбокс — зона, наносящая урон, а хёртбокс — зона, получающая урон; в 2D-играх обе обычно реализуются как триггеры.

Почему быстрый объект иногда пролетает сквозь тонкую стену?

Это туннелирование. Если за один шаг объект смещается дальше, чем толщина стены плюс его собственный размер, ни в одном кадре проверка пересечения не срабатывает. Защита — ограничить максимальную скорость, разбить шаг на подшаги или использовать проверку отрезка движения.

Как проверить, что персонаж не проваливается сквозь платформу?

Прогоните физику фиксированными шагами без графики и выведите координату нижней грани персонажа и вертикальную скорость в момент контакта — нижняя грань должна совпасть с верхом платформы, а скорость стать равной 0. Затем проверьте худший случай, максимальную скорость падения, умноженную на шаг, она должна быть меньше суммы толщины платформы и высоты персонажа.