Как делают игры: игровой цикл и FPS
Любая игра на свете — от пиксельной змейки до огромного шутера — внутри устроена как один бесконечный цикл, который крутится по 60 раз в секунду.
Игровой цикл (game loop) — бесконечно повторяющийся набор шагов «обработать ввод — обновить состояние — нарисовать кадр», на котором держится любая игра.
Зачем это нужно: почему игры вообще двигаются
Открой любую игру на телефоне — хоть змейку, хоть Brawl Stars. Герой бежит, враги ползут, цифры на счёте растут, а ты тычешь пальцем в экран. Кажется, что всё происходит само собой, плавно и одновременно. Но компьютер не умеет делать «плавно и одновременно» по-настоящему. Он умеет только одно: выполнять команды по очереди, одну за другой, очень-очень быстро.
Так как же тогда картинка движется? Секрет в том, что игра вообще не двигается. Она просто очень часто перерисовывает себя заново. Представь флипбук — блокнот, на каждой странице которого нарисован человечек чуть в другой позе. Листаешь медленно — видишь отдельные картинки. Листаешь быстро — человечек побежал. Игра — это такой же флипбук, только страницы рисует код, и листает он их по 60 штук в секунду.
К концу этого урока ты будешь смотреть на любую игру другими глазами. Ты увидишь не «волшебство», а понятный механизм: бесконечную петлю, которая снова и снова делает три простых дела. И когда дальше в курсе наш герой-цыплёнок CodeChick впервые задвигается по экрану, ты будешь точно знать, что именно заставляет его шевелиться. А пока цель скромная, но важная: понять идею, ради которой написана вообще вся игровая разработка.
Главная идея: игра — это сердцебиение
Есть метафора лучше флипбука, и она про живое. Игра похожа на сердце. Сердце не бьётся один раз и не успокаивается — оно стучит снова и снова, ровным ритмом, пока ты жив. Каждый удар — это маленький законченный цикл: сжаться, толкнуть кровь, расслабиться. Останови сердце — и организм замрёт.
В игре роль сердца играет игровой цикл. Это игровой цикл (game loop) — бесконечно повторяющийся набор шагов, на котором держится любая игра. Один «удар» этого сердца называется кадр (frame) — одна целиком нарисованная картинка игры. Пока игра запущена, цикл бьётся: кадр, кадр, кадр, кадр. Останови цикл — и игра замрёт намертво, как стоп-кадр в кино.
Игровой цикл — это сердцебиение игры. Один удар сердца = один кадр. Пока сердце бьётся, мир игры живёт.
Из чего состоит один удар: три шага кадра
Каждый удар этого сердца — не пустой. Внутри одного кадра игра всегда делает три дела, всегда в одном и том же порядке. Запомни их как «считал — подумал — нарисовал»:
- Ввод (input). Игра смотрит, что нажал игрок: куда тянет джойстик, какие клавиши зажаты, где палец на экране. Это как сначала выслушать команду.
- Обновление (update). Игра меняет состояние мира по правилам: двигает героя, проверяет столкновения, начисляет очки, отнимает жизни. Здесь живёт вся «логика» игры.
- Отрисовка (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) — время между кадрами. Это как успеть на один и тот же автобус, идя то быстрее, то медленнее. Пока просто запомни, что такая штука есть; разберём её в отдельном уроке.
Мини-практика: разбери игру на части
Кода писать пока не нужно — потренируем «игровое зрение». Возьми любую знакомую игру (хоть змейку, хоть платформер на телефоне) и разложи её на три шага кадра. Ответь себе по пунктам:
- Ввод. Что игра считывает каждый кадр? Какие клавиши, свайпы или тапы? Перечисли все способы, которыми ты управляешь героем.
- Обновление. Что меняется в мире, даже когда ты ничего не нажимаешь? Подумай: враги двигаются сами, время идёт, таймер тикает, объекты падают. Всё это — обновление состояния.
- Отрисовка. Что рисуется на экране каждый кадр? Перечисли слои: фон, герой, враги, интерфейс со счётом и жизнями.
А теперь бонус-вопрос на FPS: вспомни момент, когда игра у тебя «лагала» и герой дёргался. Как думаешь, какой из трёх шагов в тот момент не успевал уложиться в свои ~16 миллисекунд? Подсказку искать не надо — просто прикинь сам, это и есть начало настоящего игрового мышления.
Итоги
Сегодня ты разобрался с фундаментом, на котором стоит вообще любая игра:
- Игра не «двигается» — она очень часто перерисовывает себя заново, как быстро листаемый флипбук.
- Игровой цикл — это сердцебиение игры: бесконечная петля, один удар которой = один кадр.
- Каждый кадр состоит из трёх шагов в строгом порядке: ввод, обновление, отрисовка («считал — подумал — нарисовал»).
- FPS — сколько кадров в секунду рисует игра; целятся в 60, потому что так движение выглядит гладко и совпадает с обновлением экрана.
- В браузере цикл крутят не через
while, а черезrequestAnimationFrame, чтобы вкладка не зависла.
В следующем уроке мы перейдём от теории к делу: создадим настоящий холст (canvas), получим контекст 2D и впервые нарисуем на нём нашего цыплёнка. Сердце игры мы уже завели — пора показать на экране того, кто будет в ней жить.