
Введение: феномен, который никто так и не смог скопировать
В январе 2014 года мир сошёл с ума по Flappy Bird, хотя сама игра вышла ещё 24 мая 2013 года и была предельно простой. Всё, что в ней нужно делать игроку — тапать по экрану, чтобы птица не врезалась в трубы. Тем не менее игра внезапно стала вирусной, а её создатель зарабатывал на рекламе по $50 000 в день.
Но главное в этой истории не только популярность самой игры, но и её быстрый финал. Из‑за ошеломляющего успеха и давления разработчик Донг Нгуен удалил игру из магазинов приложений в феврале 2014 года, что вызвало ещё больший ажиотаж. Flappy Bird стала феноменом, который породил тысячи клонов, и даже на чёрном рынке начали продавать смартфоны с предустановленной игрой, так как самой игры в интернете больше не было.
Объясняя мотивы, подтолкнувшие его к удалению хита, Нгуен признаётся, что популярность приложения лишила его покоя — вплоть до проблем со сном. После удаления Flappy Bird он на несколько дней отключился от интернета и смог наконец нормально отдохнуть (источник — здесь объясняется подробнее, что произошло).
Но вот парадокс: при всей простоте механики почти ни один клон не смог передать то самое чувство оригинала. Разница в пикселях? В частоте обновления? В физике? Или в чём-то неуловимом, что делает игру одновременно бесящей и вызывающей зависимость?
Я подумал и решил, что нужно создать собственный веб-клон Flappy Bird и разобрать его до винтиков. Понять, что мне не нравится в нём или почему я не хочу в него играть, в отличие от оригинала. В этой статье мы заглянем в архитектуру игрового движка, разберём формулы физики, оптимизацию рендеринга и — самое интересное (по крайней мере для меня) — честно перечислим, почему браузерная версия никогда не догонит легендарный оригинал.
Часть 1. Архитектура проекта: модульный подход к ретроигре
Когда я проектировал веб-клон Flappy Bird, ключевым решением стало разделение логики. В итоге получилось, что вместо одного большого файла у нас каждый модуль живёт своей жизнью. Хотя для проектов это уже стандарт, который обеспечивает правильность кода.

Почему это важно? В теории это ни на что сильно не влияет, но для понимания и лёгкого редактирования это неизбежный и важный момент. Так вот, движок ничего не знает о том, как выглядит птица, он оперирует голыми координатами {x, y}, радиусом r и скоростью vy. Художник (Renderer) же не знает правил игры — он просто получает текущее состояние и рисует кадр.
1.2 Главный цикл и смысл всех ретроигр — цена 60 (или меньше) кадров в секунду
animationLoop() { this.gameUpdate(); // Шаг физики и логики this.render(); // Отрисовка requestAnimationFrame(() => this.animationLoop()); }
requestAnimationFrame(rAF) — это встроенный в браузер метод JavaScript, предназначенный для создания плавных анимаций. В отличие от setInterval или setTimeout, он синхронизирован с вертикальной развёрткой монитора (vsync). При 60 Hz мы получаем ровно 60 вызовов в секунду (взял определение из интернета).
Если говорить проще, то каждый такой вызов — это один игровой кадр, благодаря которому игра выглядит плавно и отзывчиво. Весь код, связанный с обновлением логики и отрисовкой, должен уложиться в промежуток между двумя кадрами. При 60 кадрах в секунду этот промежуток составляет примерно 16.6 миллисекунды (1 тыс. мс / 60 = 16.66 мс).
На современном железе это ~2–3 мс. Запас есть, но на слабых мобильных устройствах с экранами 30 Hz могут быть проблемы.
Часть 2. Физика под микроскопом: формулы и их значение
2.1 Гравитация и прыжок: почему именно эти числа?
const GRAVITY = 0.2; // пиксель/кадр² const JUMP_POWER = -5.4; // пиксель/кадр (отрицательная скорость — вверх) const MAX_VELOCITY = 9; // терминальная скорость updateBird() { this.bird.vy += GRAVITY; // v = v₀ + a·t if (this.bird.vy > MAX_VELOCITY) this.bird.vy = MAX_VELOCITY; // ограничение скорости падения this.bird.y += this.bird.vy; // Δy = v·Δt (Δt = 1 кадр) } jump() { this.bird.vy = JUMP_POWER; // мгновенный импульс (замена скорости) }
2.1.1 Ускорение свободного падения в игре
Константа GRAVITY = 0.2 означает, что за каждый кадр вертикальная скорость птицы увеличивается на 0.2 пикселя/кадр. Это покадровое ускорение, и чтобы получить привычное физическое ускорение в единицах «пиксель/секунда²», нужно умножить на количество кадров в секунде (60 кадров/сек):
2.1.2 Здесь может возникнуть вопрос: почему GRAVITY = 0.2 или откуда это число?
Так вот, значение подобрано геймдизайнером (мной) экспериментально ради баланса игровых ощущений:
Изначально я взял GRAVITY = 1.6, чтобы приблизиться к реальному ускорению 9.8 м/с² (при 1 пиксель ≈ 1 см и 60 кадрах/сек получается ≈ 9.6 м/с² — почти точно). Но при такой гравитации игра оказалась слишком медленной, птица вяло поднималась и долго падала, трудно было создать напряжение. Тогда я решил повысить показатель и путём подбора увеличил гравитацию до 0.2. Это сделало падение быстрее, что меня устраивало как игрока, когда я тестировал игру.
Таким образом, 0.2 — это не физическая константа, а игровая. И её можно спокойно увеличивать или уменьшать, т. к. она рождалась из ощущений, а не из расчётов.
2.1.3. Если я правильно помню физику, то должен быть ещё вопрос: почему прыжок не зависит от текущей скорости?
В реальной физике прыжок добавляет импульс к текущей скорости:
Если птица падает (v_до положительная), прыжок (u_отрицательный) лишь уменьшит скорость падения, но не гарантирует подъёма.
В игре же прыжок полностью заменяет скорость:
jump() { this.bird.vy = JUMP_POWER; }
Это сделано намеренно, и главная причина — это предсказуемость для игрока, то есть высота прыжка всегда одинакова, независимо от того, падала птица или поднималась.
2.1.4 Траектория полёта
Если построить график y(t) при последовательных прыжках, получится пилообразная кривая, и из-за ограничения максимальной скорости график падения становится линейным, а не параболическим, как должно быть в реальном мире, но в игре это как раз то, что добавляет предсказуемости в прыжках.
Итог: вся механика — осознанное упрощение физики ради динамичного и понятного геймплея. Цифры рождены не из учебника или формул, а из игровых тестов самого пользователя (пока это был только я).
2.2 Генерация труб: процедурный хаос
randomPipeHeight() { const max = CONFIG.H - CONFIG.PIPE_GAP - 50; // 600 - 160 - 50 = 390 const min = 50; return Math.floor(Math.random() * (max - min + 1) + min); }
2.2.1 Почему просвет именно 160 пикселей?
Высота птицы — 28 пикселей. Просвет в 160 пикселей составляет:
Просвет |
Относительный размер |
Результат |
|---|---|---|
200 px |
7.1 высота птицы |
Игра слишком лёгкая (на 20% больше пространства для ошибки) |
160 px |
5.7 высота птицы |
Оптимально — по ощущениям лучший вариант |
120 px |
4.3 высота птицы |
Почти непроходимо, игра превращается в лотерею |
Почему не больше и не меньше?
Значение 160 подобрано для более простой игры, но при тестировании мне больше понравилось 120. Также если посмотреть расстояние между трубами, то можно понять, что каждые 1.8 секунды игрок получает новое препятствие (трубу), при просвете 160 игра более спокойна, чтобы расслабиться. Но когда поставил 120, я немного почувствовал старые воспоминания, поэтому здесь нужно будет добавить два режима в будущем.
2.3 Математика бесконечной генерации
Трубы не создаются бесконечно — их пул фиксирован, как и во всех играх. Алгоритм очень простой:
Когда труба уходит за левый край экрана (
x + width < 0), она удаляется из массива.Новая труба создаётся, когда последняя труба приближается к правому краю на расстояние меньше
PIPE_SPACING.Позиция новой трубы:
x = canvas.width, а просвет (дырка) генерируется случайно в допустимых пределах.
2.4 Коллизии: геометрия немного нестандартная
checkCircleRectCollision(cx, cy, r, rx, ry, rw, rh) { let closestX = Math.max(rx, Math.min(cx, rx + rw)); let closestY = Math.max(ry, Math.min(cy, ry + rh)); let dx = cx - closestX; let dy = cy - closestY; return (dx * dx + dy * dy) < r * r; }
Возможно, возникает вопрос: а почему не AABB (прямоугольник — прямоугольник)?

Так вот, если бы мы проверяли столкновение по прямоугольному bounding box птицы (28×28 пикселей), то:
Углы труб имели бы пустые зоны (визуально птица ещё не коснулась трубы).
Игрок бы получал «фантомные» смерти.
Математика метода «круг — прямоугольник»
Находим ближайшую точку прямоугольника к центру круга — через проекцию центра на оси прямоугольника с последующим ограничением (clamp).
Вычисляем евклидово расстояние от центра круга до этой точки.
Если расстояние меньше радиуса, коллизия есть.
Важный нюанс, что порядок проверки происходит после обновления позиции. Если за один кадр птица «перелетела» сквозь трубу (была слева, а стала справа, ни разу не задев стенки), мы всё равно засчитываем успех.
Часть 3. Визитка движка самой игры
3.1 Отрисовка птицы
В классическом Flappy Bird птица была квадратной — 8-битная эстетика рулила. В моей версии птица круглая и немного напоминает Angry Birds, но дух игры сохраняется. Вот как рисуется тело птицы из файла skinManager.js:
ctx.beginPath(); // начинаем рисовать ctx.ellipse(0, -1, config.BIRD_RADIUS - 1, config.BIRD_RADIUS, 0, 0, Math.PI * 2); // тело ctx.fillStyle = skin.bodyColor1; // цвет тела ctx.fill(); // закрашиваем
Почему именно так?
1. Честные коллизии.
Птица — круг радиусом config.BIRD_RADIUS. Отрисовка строго соответствует геометрии, проверяемой в checkCollision(). Игрок видит ровно ту форму, которая участвует в столкновениях.
2. Простота кастомных скинов.
Система скинов (skins объект) подменяет только цвета. Форма остаётся неизменной, поэтому любой скин автоматически корректен с точки зрения коллизий и физики.
3.2. Анимация крыльев, или, по-другому, конечный автомат в действии
Вот сердце анимации — функция updateWingAngle():
function updateWingAngle() { if (isFlapping) { wingAngle = Math.sin(flapTimer * 0.03) * 1.4; flapTimer++; if (flapTimer > 55) { isFlapping = false; flapTimer = 0; } } else { wingAngle += 0.025 * wingDirection; if (wingAngle > 0.5) { wingAngle = 0.5; wingDirection = -1; } else if (wingAngle < -0.5) { wingAngle = -0.5; wingDirection = 1; } } }
Математика плавного движения
В состоянии покоя крыло движется линейно. Код прибавляет +0.025 или -0.025 к wingAngle на каждом кадре в зависимости от wingDirection. При достижении границ ±0.5 направление меняется на противоположное.
Математика резкого взмаха
Когда игрок нажимает пробел или тап, вызывается triggerWingFlap(), которая устанавливает:
isFlapping = true; // включаем режим прыжка flapTimer = 0; // сбрасываем таймер wingAngle = 0.9; // мгновенный стар
Дальше каждый кадр вычисляется по формуле:
Часть 4. Минусы нашего клона (в коде)
4.1 Проблема №1: Нет предсказания ввода (Input Prediction)
В нативных играх есть понятие input prediction. Если игра видит, что игрок нажал на экран за 5 мс до столкновения, она может «простить» ошибку, откатив время на 1 кадр.
У нас строгая проверка, что если коллизия есть, то Game Over.
4.2 Проблема №2: Звук или его отсутствие
В игре нет вообще звуков. В оригинале есть щелчок при прыжке или «бдыщь» при столкновении — это создаёт почти 50% ощущений. Без аудиообратной связи игра кажется «плоской».
Почему не добавить? Так вот, в Web Audio API требует запроса разрешения от пользователя (из-за автополитики браузеров), без жеста пользователя звук не воспроизвести, поэтому довольно часто этот пункт пропускают при веб-разработке.
Часть 5. Логические проблемы: почему игра не вызывает азарт
Технические недостатки — это полбеды. Главная проблема моего клона в том, что он не вызывает зависимости. Разберём почему.
5.1 Проблема №7: Нет «эффекта потока»
Оригинал был удалён, потому что вызывал зависимость. Там был идеальный цикл, который в психологии называется «поток».
Критерий потока |
Оригинал |
|---|---|
Низкий порог входа |
Один клик — и ты в игре |
Быстрая обратная связь |
Смерть за 1-2 секунды |
Мгновенный рестарт |
Повторный клик по экрану |
Отсутствие барьеров |
Нет загрузок, нет меню |
Психологический механизм: Человек тратит 2 секунды на попытку, 0.1 секунды на осознание ошибки и 0.2 секунды на рестарт. Цикл занимает 2.3 секунды. В моём клоне: 2 секунды на попытку, 1 секунда на появление меню, 1 секунда на рестарт — цикл в 2 раза длиннее.
5.2 Проблема №8: Нет стиля и «души» как в Mario
В игре хоть автор говорит, что стиль не как не связан с Mario, но всё же у обычного пользователя (как я) оно появилось, поэтому здесь проблема, что стиль создаёт эмоциональную привязку, а стиль одной из лучших игр — это всегда путь к успеху.
5.3 Проблема №9: Неправильный темп игры (Tempo Mismatch)
В оригинальной Flappy Bird расстояние между трубами и скорость птицы подобраны так, чтобы игрок постоянно находился в состоянии лёгкого стресса.
У меня же игра превращается в плавный полёт бумажного самолётика. Игрок кинул, посмотрел, надоело, ушёл. Нет того самого «ещё одну попытку».
5.4 Проблема №10: Нет прогрессии сложности
В оригинале после 10, 20, 30 очков игра не менялась. Это было гениально, т. к. игрок знал, что если он прошёл 5 труб, то 10-ю пройдёт по той же формуле. Сложность не растёт — растёт только давление от счёта.
В моём клоне тоже нет прогрессии. Но почему это проблема, если в оригинале его тоже не было? Так вот, ответ следует из 9 проблемы, а конкретно в скорость, если у тебя спокойная игра, то игрок быстро находит «золотую середину» и играет на автомате, и так как игра плавная, то не ощущения собственного прогресса: «Раньше я умирал на 5 очках, а теперь на 20!».
5.5 Проблема №11: Нет социального доказательства или ажиотажа толпы и видео
Flappy Bird взлетела из-за вирусного эффекта. Люди постили скриншоты со счётом 2, 5, 10 — и смеялись над собой. Было стыдно за низкий счёт, но это рождало соревнование.
5.6 Проблема №12: Наказание слишком жестокое
В оригинале после смерти игра мгновенно перезапускалась. Игрок не успевал расстроиться — его палец уже снова жал на экран.
Психологическая ловушка: Чем быстрее рестарт, тем меньше времени на осознание поражения. Не успевает подумать «какая глупая игра» — он уже в следующей попытке.
6. Итоги
6.1 Что получилось в итоге.


6.2 Заключение
Этот проект — напоминание о перформансе, который никто не смог повторить. Наша цель, конечна, не полностью реализована, но это не значит, что веб-клон бесполезен это просто ещё один клоун хорошей игры.
Flappy Bird — идеальный пример того, как маленькие детали создают великую игру. Не физика и не графика, а психология: правильный баланс между сложностью и напряжением, мгновенная обратная связь, отсутствие барьеров для повторной попытки.
Наш код рабочий, играбельный и даже интересный. Но чтобы повторить успех оригинала, нужно копать глубже — на уровень психологии.
Поиграть в мою версию «Flappy Bird» можно здесь.
Исходный код открыт на GitHub — используйте его как основу для своих творческих экспериментов.
Если у вас есть идеи по улучшению проекта, вы нашли баг или просто хотите вспомнить, как впервые набили 10 очков в «Flappy Bird», пишите в комментариях
© 2026 ООО «МТ ФИНАНС»