Как делают игры: игровой цикл и FPS

Любая игра на свете — от пиксельной змейки до огромного шутера — внутри устроена как один бесконечный цикл, который крутится по 60 раз в секунду.

Игровой цикл (game loop) — бесконечно повторяющийся набор шагов «обработать ввод — обновить состояние — нарисовать кадр», на котором держится любая игра.

Зачем это нужно: почему игры вообще двигаются

Открой любую игру на телефоне — хоть змейку, хоть Brawl Stars. Герой бежит, враги ползут, цифры на счёте растут, а ты тычешь пальцем в экран. Кажется, что всё происходит само собой, плавно и одновременно. Но компьютер не умеет делать «плавно и одновременно» по-настоящему. Он умеет только одно: выполнять команды по очереди, одну за другой, очень-очень быстро.

Так как же тогда картинка движется? Секрет в том, что игра вообще не двигается. Она просто очень часто перерисовывает себя заново. Представь флипбук — блокнот, на каждой странице которого нарисован человечек чуть в другой позе. Листаешь медленно — видишь отдельные картинки. Листаешь быстро — человечек побежал. Игра — это такой же флипбук, только страницы рисует код, и листает он их по 60 штук в секунду.

К концу этого урока ты будешь смотреть на любую игру другими глазами. Ты увидишь не «волшебство», а понятный механизм: бесконечную петлю, которая снова и снова делает три простых дела. И когда дальше в курсе наш герой-цыплёнок CodeChick впервые задвигается по экрану, ты будешь точно знать, что именно заставляет его шевелиться. А пока цель скромная, но важная: понять идею, ради которой написана вообще вся игровая разработка.

Главная идея: игра — это сердцебиение

Есть метафора лучше флипбука, и она про живое. Игра похожа на сердце. Сердце не бьётся один раз и не успокаивается — оно стучит снова и снова, ровным ритмом, пока ты жив. Каждый удар — это маленький законченный цикл: сжаться, толкнуть кровь, расслабиться. Останови сердце — и организм замрёт.

В игре роль сердца играет игровой цикл. Это игровой цикл (game loop) — бесконечно повторяющийся набор шагов, на котором держится любая игра. Один «удар» этого сердца называется кадр (frame) — одна целиком нарисованная картинка игры. Пока игра запущена, цикл бьётся: кадр, кадр, кадр, кадр. Останови цикл — и игра замрёт намертво, как стоп-кадр в кино.

Игровой цикл — это сердцебиение игры. Один удар сердца = один кадр. Пока сердце бьётся, мир игры живёт.

Из чего состоит один удар: три шага кадра

Каждый удар этого сердца — не пустой. Внутри одного кадра игра всегда делает три дела, всегда в одном и том же порядке. Запомни их как «считал — подумал — нарисовал»:

  1. Ввод (input). Игра смотрит, что нажал игрок: куда тянет джойстик, какие клавиши зажаты, где палец на экране. Это как сначала выслушать команду.
  2. Обновление (update). Игра меняет состояние мира по правилам: двигает героя, проверяет столкновения, начисляет очки, отнимает жизни. Здесь живёт вся «логика» игры.
  3. Отрисовка (render). Игра рисует получившийся мир на экране: фон, героя, врагов, счёт. Это и есть та самая страница флипбука, которую ты увидишь.

Порядок важен и логичен: сначала узнать, чего хочет игрок, потом сдвинуть по этому миру всё, что надо, и только потом показать результат. Нельзя рисовать раньше, чем подумал, — иначе нарисуешь старую, ещё не обновлённую картинку. И эти три шага повторяются в каждом кадре, десятки раз в секунду, без остановки.

Что такое FPS и почему именно 60

Раз кадры сменяют друг друга, логично спросить: а как часто? Тут и появляется самое известное игровое словечко. FPS (кадры в секунду) — сколько раз в секунду игра перерисовывает картинку. Если игра выдаёт 60 FPS, значит её сердце бьётся 60 раз каждую секунду, и ты видишь 60 свежих кадров.

Почему целятся именно в 60? Потому что обычные экраны телефонов и мониторов чаще всего обновляются 60 раз в секунду. Рисовать чаще — бессмысленно, экран всё равно не покажет лишние кадры. А вот глаз человека устроен так, что при 60 кадрах движение выглядит абсолютно гладким — мозг уже не различает отдельные картинки и видит сплошное движение. Это та самая планка, на которой игра ощущается «дорого» и приятно.

А что если кадров меньше? Вот тут начинается знакомая боль. Сравни на ощущениях:

FPSКак ощущается
60Идеально гладко, движение как в жизни
30Терпимо, но в динамике заметна лёгкая «ступенчатость»
15Дёргается, играть некомфортно
5Слайд-шоу, в это невозможно играть

Когда в онлайн-игре «лагает» и герой телепортируется рывками — это часто и есть просадка FPS: сердце игры стало биться реже, кадров не хватает, и флипбук листается медленно. Поэтому 60 FPS — не каприз, а цель, ради которой разработчики стараются, чтобы три шага кадра успевали выполниться достаточно быстро.

Маленькая ловушка: 60 FPS — это очень мало времени

Прикинем в уме. Если за секунду нужно сделать 60 кадров, то на один кадр остаётся примерно 1 / 60 секунды — это около 16 миллисекунд. За эти 16 миллисекунд игра обязана успеть и ввод обработать, и весь мир обновить, и всё нарисовать. Звучит как мало времени — и это правда мало. Поэтому игровой код стараются писать так, чтобы внутри кадра не делать ничего лишнего и тяжёлого. Но об оптимизации речь пойдёт сильно позже; сейчас просто прочувствуй ритм: успеть всё за один удар сердца, и так 60 раз в секунду.

Разбираем на примерах

Договоримся сразу: игры рисуются на холсте в браузере и в нашем учебном раннере по-настоящему не запускаются. Поэтому код ниже помечен как подсветка без кнопки «Запустить», а под каждым блоком идёт словесное описание того, что происходило бы на экране. Читай код глазами и представляй кадр.

Пример 1. Цикл, который никогда не кончается

Сначала самый честный, «учебный» вид игрового цикла — обычный бесконечный while. Так его рисуют в книжках, чтобы показать суть.

// Псевдокод: вечная петля игры
while (true) {
  const input = readInput();   // 1. что нажал игрок
  updateWorld(input);          // 2. сдвинули мир по правилам
  drawFrame();                 // 3. нарисовали кадр на экране
}
// сюда выполнение никогда не дойдёт

Результат: на экране без остановки сменяют друг друга кадры игры. Цикл крутится вечно: считал ввод, обновил мир, нарисовал — и снова сначала. Игра «живёт», пока эта петля вертится; строчка после цикла не выполнится никогда.

Разберём по шагам. while (true) — это петля, у которой условие выхода всегда истинно, поэтому она не заканчивается сама. Внутри ровно те самые три шага кадра: readInput() собирает нажатия, updateWorld(input) двигает героя и проверяет правила, drawFrame() рисует картинку. Имена функций тут выдуманные — нам важна структура, а не конкретные команды. Главное, что ты видишь скелет любой игры: три действия в петле, по кругу.

Пример 2. Почему чистый while в браузере не годится

А теперь честная оговорка. Такой while (true) в браузере писать нельзя — он намертво повесит вкладку. Браузеру нужно между кадрами успевать рисовать, реагировать на скролл и не «зависнуть». Поэтому в вебе ритм задаёт специальная браузерная функция. requestAnimationFrame — браузерная функция, которая вызывает наш код перед каждой перерисовкой экрана и задаёт ритм игрового цикла.

// Настоящий игровой цикл в браузере
function gameLoop() {
  const input = readInput();   // 1. ввод
  updateWorld(input);          // 2. обновление
  drawFrame();                 // 3. отрисовка

  requestAnimationFrame(gameLoop); // попросить браузер вызвать нас снова
}

requestAnimationFrame(gameLoop); // запускаем первый кадр

Результат: игра идёт так же плавно, как в примере 1, но вкладка не виснет. После каждого нарисованного кадра функция вежливо просит браузер: «вызови меня снова перед следующей перерисовкой». Браузер делает это примерно 60 раз в секунду — ровно в такт обновлению экрана.

Что изменилось по сути? Вместо того чтобы крутить петлю самим и не отпускать управление, мы делаем один кадр и в конце вызываем requestAnimationFrame(gameLoop) — это значит «брось мне обратно на следующий кадр». Браузер сам подбирает момент, синхронный с обновлением экрана, и снова вызывает нашу gameLoop. Получается тот же бесконечный цикл, но управляемый браузером: и плавно, и вкладка живая. Именно так устроены почти все браузерные игры, и наш цыплёнок дальше будет бегать в точно таком же цикле.

Пример 3. Три шага наглядно, на нашем цыплёнке

Покажем те же три шага, но уже с намёком на нашего героя — чтобы было видно, что где живёт.

// chicken — это наш герой, его состояние
let chicken = { x: 50, y: 100, speed: 3 };

function gameLoop() {
  // 1. ВВОД: зажата ли стрелка вправо?
  const goRight = isKeyDown('ArrowRight');

  // 2. ОБНОВЛЕНИЕ: двигаем цыплёнка по правилам
  if (goRight) {
    chicken.x = chicken.x + chicken.speed;
  }

  // 3. ОТРИСОВКА: рисуем фон и цыплёнка на новом месте
  clearScreen();
  drawChicken(chicken.x, chicken.y);

  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

Результат: на экране стоит жёлтый цыплёнок. Пока ты держишь стрелку вправо, он плавно едет вправо — на 3 пикселя каждый кадр, то есть примерно на 180 пикселей в секунду при 60 FPS. Отпустил клавишу — цыплёнок замер на месте.

Теперь видно, где живёт каждый из трёх шагов. Ввод — строка с isKeyDown('ArrowRight'): спрашиваем, зажата ли клавиша. Обновлениеif (goRight): если да, прибавляем к координате chicken.x скорость. Заметь, мы здесь только меняем числа, ничего ещё не рисуя. ОтрисовкаclearScreen() и drawChicken(...): стираем старый кадр и рисуем цыплёнка в его новой позиции. И в самом конце — requestAnimationFrame(gameLoop), чтобы всё повторилось со следующим кадром.

Запомни имя chicken и то, что у него есть x, y и speed — этот наш герой и его набор данных будут кочевать из урока в урок. Сейчас он умеет ехать вправо; дальше научится прыгать, анимироваться и сталкиваться с врагами, но цикл вокруг него останется тем же самым.

Частые ошибки и подводные камни

Вот грабли, на которые наступают почти все, кто только взялся за игровой цикл. Прочитай заранее.

  • Писать while (true) прямо в браузере. Чистая бесконечная петля не отдаёт управление браузеру, и вкладка намертво виснет — ты даже не сможешь её закрыть нормально. В вебе цикл всегда крутят через requestAnimationFrame, а не голым while.
  • Рисовать раньше, чем обновил. Если поменять порядок и сначала вызвать отрисовку, а потом обновление, на экране окажется состояние мира из прошлого кадра. Картинка будет отставать от логики на один кадр. Держи порядок строго: ввод, обновление, отрисовка.
  • Забыть стереть прошлый кадр. Если не очищать холст в начале отрисовки, новый кадр ляжет поверх старого, и за цыплёнком потянется длинный «хвост» из его прошлых позиций. Иногда это красивый эффект, но если он не задуман — чисти экран первой командой отрисовки.
  • Думать, что 60 FPS гарантированы. Если внутри кадра делать слишком много тяжёлой работы, она не успеет уложиться в ~16 миллисекунд, и FPS просядет — игра начнёт дёргаться. Сердце игры тоже можно «загнать», если на каждый удар вешать слишком много дел.
  • Считать, что движение зависит только от скорости. Мы прибавляли speed каждый кадр. Но на медленном устройстве кадров в секунду меньше, и тогда тот же герой проедет за секунду меньше. Чтобы движение было одинаковым при любом FPS, скорость умножают на дельта-время (delta time) — время между кадрами. Это как успеть на один и тот же автобус, идя то быстрее, то медленнее. Пока просто запомни, что такая штука есть; разберём её в отдельном уроке.

Мини-практика: разбери игру на части

Кода писать пока не нужно — потренируем «игровое зрение». Возьми любую знакомую игру (хоть змейку, хоть платформер на телефоне) и разложи её на три шага кадра. Ответь себе по пунктам:

  1. Ввод. Что игра считывает каждый кадр? Какие клавиши, свайпы или тапы? Перечисли все способы, которыми ты управляешь героем.
  2. Обновление. Что меняется в мире, даже когда ты ничего не нажимаешь? Подумай: враги двигаются сами, время идёт, таймер тикает, объекты падают. Всё это — обновление состояния.
  3. Отрисовка. Что рисуется на экране каждый кадр? Перечисли слои: фон, герой, враги, интерфейс со счётом и жизнями.

А теперь бонус-вопрос на FPS: вспомни момент, когда игра у тебя «лагала» и герой дёргался. Как думаешь, какой из трёх шагов в тот момент не успевал уложиться в свои ~16 миллисекунд? Подсказку искать не надо — просто прикинь сам, это и есть начало настоящего игрового мышления.

Итоги

Сегодня ты разобрался с фундаментом, на котором стоит вообще любая игра:

  • Игра не «двигается» — она очень часто перерисовывает себя заново, как быстро листаемый флипбук.
  • Игровой цикл — это сердцебиение игры: бесконечная петля, один удар которой = один кадр.
  • Каждый кадр состоит из трёх шагов в строгом порядке: ввод, обновление, отрисовка («считал — подумал — нарисовал»).
  • FPS — сколько кадров в секунду рисует игра; целятся в 60, потому что так движение выглядит гладко и совпадает с обновлением экрана.
  • В браузере цикл крутят не через while, а через requestAnimationFrame, чтобы вкладка не зависла.

В следующем уроке мы перейдём от теории к делу: создадим настоящий холст (canvas), получим контекст 2D и впервые нарисуем на нём нашего цыплёнка. Сердце игры мы уже завели — пора показать на экране того, кто будет в ней жить.

Проверьте себя
1. Как на самом деле устроено движение в любой игре?
AКартинка действительно плавно перемещается по экрану сама по себе
BИгра очень часто перерисовывает себя заново, как быстро листаемый флипбук
CДвижение записано заранее как видео, которое игра проигрывает
DЭкран сам сдвигает пиксели без участия кода
2. Из каких трёх шагов состоит один кадр игрового цикла и в каком порядке?
AОтрисовка, ввод, обновление
BОбновление, отрисовка, ввод
CВвод, обновление, отрисовка
DВвод, отрисовка, обновление
3. Что такое FPS?
AСколько раз в секунду игра перерисовывает картинку
BСколько врагов помещается на экране одновременно
CСкорость интернет-соединения в онлайн-игре
DКоличество кнопок на джойстике
4. Почему в браузерных играх обычно целятся именно в 60 FPS?
AЭто максимум, который вообще способен нарисовать любой компьютер
BБольше 60 кадров человек физически не различает ни при каких условиях
CОбычные экраны обновляются 60 раз в секунду, и при 60 FPS движение выглядит гладко
D60 — просто красивое число, выбранное случайно
5. Почему в браузере нельзя крутить игровой цикл через обычный while (true)?
Awhile (true) работает слишком медленно для игр
BТакая петля не отдаёт управление браузеру, и вкладка намертво виснет
CJavaScript вообще не поддерживает цикл while
Dwhile рисует кадры в неправильном порядке
6. Зачем в конце функции gameLoop вызывают requestAnimationFrame(gameLoop)?
AЧтобы попросить браузер вызвать gameLoop снова перед следующей перерисовкой экрана
BЧтобы немедленно остановить игру и закрыть вкладку
CЧтобы один раз нарисовать кадр и больше не повторять
DЧтобы увеличить FPS выше возможностей экрана