Коротко
Игровой цикл (Game Loop) — это повторяющаяся последовательность «прочитать ввод → обновить мир → нарисовать кадр», на которую при частоте 60 к/с отводится 16,67 мс, а при 30 к/с — 33,33 мс. Чтобы физика вела себя одинаково на любом компьютере, её обновляют фиксированными шагами через аккумулятор времени, а движение умножают на шаг времени. Столкновения прямоугольников без поворота (AABB — Axis-Aligned Bounding Box) проверяются четырьмя сравнениями и разрешаются выталкиванием по оси наименьшего перекрытия. В статье — формулы, готовый псевдокод, разница между коллайдером, триггером и хитбоксом и проверенный в Node.js код приземления и сбора монеты.
Содержание
- Три фазы игрового цикла и бюджет кадра в миллисекундах
- deltaTime: почему без него игра работает быстрее на мониторах с высокой частотой обновления
- Фиксированный шаг физики и аккумулятор времени
- AABB: проверка пересечения двух прямоугольников
- Разрешение столкновения: выталкивание по оси X, затем по оси Y
- Коллайдер, триггер, хитбокс и хёртбокс: чем они отличаются
- Туннелирование: почему быстрые объекты проходят сквозь стены
- Практический сценарий: проверяем приземление и сбор монеты в коде на JavaScript
- Когда вам это не подойдёт
Три фазы игрового цикла и бюджет кадра в миллисекундах
Игровой цикл каждые 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, а при приземлении обнуляется только вертикальная скорость.
Коллайдер, триггер, хитбокс и хёртбокс: чем они отличаются
Коллайдер физически не пускает объект внутрь, а триггер только сообщает о пересечении и пропускает объект насквозь.
| Тип зоны | Физически выталкивает? | Что делает при пересечении | Пример на сцене |
|---|---|---|---|
| Твёрдый коллайдер | Да | Останавливает движение, обнуляет скорость по оси удара | платформа, стена, пол |
| Триггер | Нет | Запускает событие один раз при входе или каждый кадр внутри зоны | монета, ключ, флаг, зона падения |
| Хитбокс (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 игровой движок; он в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).
Читайте также: