📱 Подписаться
IT и цифровая трансформация

Flappy Bird: делаем игру сложнее и добавляем автопилот на чистой математике

📰 Habr 👁️ 0 просмотров

Laborant_Code3 часа назад

Flappy Bird: делаем игру сложнее и добавляем автопилот на чистой математике

Уровень сложностиСреднийВремя на прочтение7 минОхват и читатели4.7KБлог компании RUVDS.comВеб-разработка*Игры и игровые консолиПрограммирование*JavaScript*КейсВведение

Начнём с предыстории. Когда я опубликовал первую статью про клон Flappy Bird, я хотел получить результат, который был бы мне приятен, но вместо этого получил всего понемногу. Поучил физику, посмотрел, как лучше работать с рендерингом, узнал, почему птица стала такой популярной игрой, но один комментарий заставил задуматься:

Один читатель написал: «Году так в 2004 у меня в кнопочном телефоне от LG была предустановлена ровно такая же игра, только вместо птицы там подводная лодка была. Так что тут не игра великая, и даже её идея далека от того, чтобы быть оригинальной» — Einherjar (никнейм автора комментария).И я считаю, что он прав. Механику с препятствиями и одним нажатием придумали задолго до вьетнамского разработчика Донга Нгуена. Я и сам помню похожие игры, но мне казалось, что они появились после Flappy Bird. Сейчас понимаю, что это не так.

И вот сижу я после публикации, перечитываю статью, комментарии и понимаю: технически игра получилась, она работает, но внутри — пустота, как говорил Альберт Эйнштейн:

«Самое прекрасное, что мы можем испытать — это ощущение тайны. Она источник всякого подлинного искусства и науки»И если так подумать, у меня не было ощущения тайны: в игру я ничего не добавил, стиль похож, и ничего нового нет и не было, нет той самой магии. И я решил, что надо сделать что-то своё, уникальное. Может, не уникальное в плане идеи, но уникальное в плане стандартного представления игры.

Почему простого клона недостаточно

Как я уже рассуждал выше, я мог бы остановиться и сказать: «Ну, клон готов, расходимся». Однако что-то внутри требовало продолжения. Понимаете это чувство, когда проект вроде закончен, а финального удовлетворения нет (сейчас у меня таких три проекта, считая этот, но один очень сложный и нет сил, а второй новый, поэтому до него ещё дойдёт время)? Как будто дорисовал картину, но забыл подписать её, сделать свой уникальный штрих.

Сначала я подумал про нейросети, т. к. это сейчас в тренде, и каждый, наверное, в первую очередь для автоматизации чего-либо подумал бы подключить нейросеть. И я не исключение: сначала даже подобрал, что и где буду писать. Конечно, на всем известном TensorFlow, обучение с подкреплением, Q-learning — звучит круто, по-взрослому. Два вечера я копался в документации, запускал примеры, пытался подружить TensorFlow.js с браузером. И вечером второго дня поймал себя на мысли: «Использовать силу Звезды Смерти, чтобы зажечь сигарету?» (небольшая отсылка на Star Wars)

Упоминание использования силы Звезды Смерти для прикуривания является художественной метафорой, отражающей абсурдность применения избыточно мощных инструментов для простых задач, и не является пропагандой курения, использования боевых станций или любого другого оружия массового поражения. (на всякий случай напишу это)Птица, труба, гравитация, прыжок — четыре переменные, то есть её очень просто рассчитать, траектория считается по школьной формуле v = v₀ + gt. И, думаю, многим придёт логичный вопрос: а зачем здесь нейросеть, которая будет методом тыка подбирать то, что компьютер может рассчитать за пару миллисекунд?

И здесь меня осенило: а что, если вместо чёрного ящика нейросети написать честный математический перебор? Как в шахматных движках, где просчитывают все варианты на несколько ходов вперёд, оценивают каждый и выбирают лучший, — стандартная практика. Без обучения, без датасетов, без GPU — чистая школьная физика и комбинаторика. Так родилась идея простого автопилота.

А что дальше ?

Но тут возникла новая проблема: если я добавлю бота к текущей версии игры, он будет проходить её с закрытыми глазами, бесконечно и некрасиво.

Боту нужен вызов, а игроку зрелищ, которые будут приносить интерес, чтобы игрок смотрел на бота, — и говорил зрелище что надо (простите, немного увлёкся). Мне, как разработчику, — интересная задача в первую очередь.

Вот так я начал усложнять игру. Сначала трубы задвигались вверх-вниз, на небольшое расстояние, потом полетели красные шары навстречу, и финальное это включилась погода — шторм и ночь.

Основная часть

Прогрессия сложности: почему статичная игра — это скучно

В оригинальной Flappy Bird сложность не росла, что меня удивляет до сих пор, т. к. даже без усложнений она смогла побить многие топы. Грубо говоря, первая труба ничем не отличалась от последней. И почему после проигрыша хотелось играть ещё и ещё? Это было просто, может, именно этим она и нравилась, конечно, своим минимализмом.

Возьмём моего клона. Вроде всё так же, но мне не хотелось играть. Как я писал в прошлой статье, физика получилась слишком плавной — игрок быстро находил «золотую середину», и набрал он 20 очков или 50 — разницы не было. Тогда нужно как-то разнообразить игру и сделать так, чтобы игрок, который дошёл до 50 очков, считал себя крутым и хотел идти дальше и дальше.

Начнём объяснение с самого простого элемента, а не с первого.

Движущиеся трубы: математика синуса

Первое, что я добавил, — вертикальное движение труб. В моей логике это вполне логичное добавление: такие препятствия не были новинкой, но почти всегда встречаются в классических играх. И вот после 20 очков каждая труба начинает колебаться по синусоиде:

const offset = Math.sin(this.pipeMovePhase + pipe.x * 0.01) * amplitude;
pipe.topHeight = pipe.baseTopHeight + offset;
pipe.bottomY = pipe.topHeight + PIPE_GAP;Почему синус, а не, скажем, случайное движение? Случайность в играх такого типа — это фрустрация. Игрок должен иметь возможность предсказать движение, пусть и с трудом, и у него всегда должен быть проход. Синусоида, как мы знаем, всегда даёт плавное, периодическое движение, которое можно выучить или спрогнозировать. Но чтобы трубы двигались не по одному шаблону, я добавил для каждой трубы свою зависимость pipe.x. Также, чтобы было всё сложнее, добавил, что на 20 очках амплитуда равна 25, а на 50 и 80 очках максимальная амплитуда становится ещё больше, чем раньше. В моём понимании, в конце у нас просвет в 150 пикселей превращается в движущуюся цель размером 150 − 80 = 70 пикселей, что делает игру динамичнее.

getCurrentAmplitude() {
const levelsAbove = Math.floor((this.score - 20) / 10);
return Math.min(25 + levelsAbove * 8, 80);
}

getCurrentMoveSpeed() {
const levelsAbove = Math.floor((this.score - 20) / 10);
return Math.min(0.8 + levelsAbove * 0.3, 3.5);
}

Новые вид и сейфзона

Летящие препятствия

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

updateObstacles() {
if (this.score >= CONFIG.OBSTACLES_START_SCORE && !this.isNightTime()) {
const spawnBonus = Math.floor((this.score - 50) / 10) * 0.003;
const effectiveChance = CONFIG.OBSTACLE_SPAWN_CHANCE + spawnBonus;

if (Math.random() < effectiveChance) { this.spawnObstacle(); } }

for (let i = this.obstacles.length - 1; i >= 0; i--) {
this.obstacles[i].x -= this.obstacles[i].speed;
if (this.obstacles[i].x < -50) {
this.obstacles.splice(i, 1);
}
}
}Коллизия считается как пересечение двух окружностей — птицы и шара. Если произошло касание, то следует моментальное поражение:

checkCircleCollision(x1, y1, r1, x2, y2, r2) {
const dist = Math.sqrt((x1-x2)**2 + (y1-y2)**2);
return dist < (r1 + r2);
}

Эффект времени суток

С 70 очков включается и помогает игроку смена обстановки, чтобы глаз отдохнул: проём между трубами растёт, ночью они двигаются с половинной амплитудой, проём становится шире на 30 px, а шары не появляются. Срабатывает раз в 10–25 очков. Весь этот момент нужен как передышка для продолжения пути.

Автопилот: самая важная часть и сложная

Как писал раньше, я не использовал машинное обучение или нейронные сети, и вместо этого бот работает как шахматный движок: перебирает варианты и выбирает лучший.

У бота есть только:

• Текущая позиция птицы (y) и скорость (vy).
• Время до трубы.
• Границы просвета с учётом предсказанного движения трубы.Нужно найти последовательность действий (прыжок/ждать) для каждого кадра, которая приведёт птицу в безопасную зону максимально близко к центру, т. к. уже было много попыток, где птица делала прыжок в трубе, чуть-чуть задевала задней частью трубу.

Предсказание движения трубы

Первый шаг после обычного автопилота — это понять, где будет труба через N кадров, т. к. хотелось бы, чтобы автопилот работал и на движущихся трубах. Для этого бот использует ту же математику, что и движок:

predictPipePosition(pipe, frames) {
const futureX = pipe.x - pipeSpeed * frames;
const futurePhase = this.engine.pipeMovePhase + moveSpeed * 0.02 * frames;
const predictedOffset = Math.sin(futurePhase + futureX * 0.01) * amplitude;
const predictedTopHeight = pipe.baseTopHeight + predictedOffset;
return { topHeight: predictedTopHeight, bottomY: predictedTopHeight + 150 };
}Бот буквально спрашивает у физического движка: «если я промотаю время на N кадров, где будет труба?»

Перебор вариантов

Дальше бот генерирует все возможные последовательности прыжков:

for (let numJumps = 0; numJumps <= maxJumps; numJumps++) {
const jumpTimings = generateJumpTimings(numJumps, totalFrames, minGap);

for (const timings of jumpTimings) {
const result = simulateWithJumps(y, vy, totalFrames, timings);
const score = evaluatePlan(result);
if (score > bestScore) bestPlan = { actions, score };
}
}На 6 кадрах до трубы с 4 возможными прыжками это даёт 15-150 комбинаций вместо 400+, если брать просто перебором.

Экстренный режим (нужен, чтобы меньше было расчётов)

Если бот оказывается ниже safe zone и падает (или выше и поднимается к потолку), он не ждёт расчёта планов — прыгает немедленно:

Пример safe_zoneneedsEmergencyJump(y, vy, safeTop, safeBottom) {
if (y > safeBottom && vy > 0) return true;
if (y < 60 && vy <= 0) return true;
return false;
}

Что не получилось и куда можно развивать

Проблема сходимости

Бот отлично проходит статические трубы, но на высоких уровнях с движущимися трубами и препятствиями иногда зацикливается. Скорее всего, причина в том, что предсказание позиции трубы становится неточным на длинных дистанциях, а перебор 100 планов не всегда находит глобальный оптимум. Возможно, нужно немного поменять логику, и всё пойдёт.

Нет обучения

Это, конечно, логичный момент, но с ним ничего не сделать, т. к. бот не запоминает свои ошибки — каждая попытка для него чистый лист.

Препятствия усложняют планирование

И последняя проблема — это сложность. Алгоритм проверяет столкновения с препятствиями только для финальной позиции плана, а не для каждого кадра по отдельности. Нужно придумать компромисс между точностью и производительностью.

Заключение

Что получилось в итоге

Игра (бот проходит трубы с шарами)Игра (бот проходит трубы ночью)Я начал этот проект с желания «доделать» — добавить тот самый уникальный штрих, которого не хватило в первой статье, и, мне кажется, у меня это получилось. От простого клона мы пришли к игре с прогрессией, где классическая механика получила новые элементы и перестала быть просто очередной копией.

Главный вывод

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

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

Код и демо

Поиграть в мою версию можно здесь. Бот включается клавишей «B».

Код проекта открыт на GitHub. Используйте его как хотите: для своих экспериментов, обучения, или просто чтобы посмотреть, как прыгает птичка без вашей помощи.

P.S Если у вас есть идеи по улучшению, вы нашли баг или хотите предложить новые идеи — пишите в комментариях.

© 2026 ООО «МТ ФИНАНС»Теги:• Flappy Bird
• Canvas
• JavaScript
• автопилот
• математический перебор
• ретро-игры
• геймдев
• бот без нейросети
• ruvds_статьи
• ФабиХабы:• Блог компании RUVDS.com
• Веб-разработка
• Игры и игровые консоли
• Программирование
• JavaScript

Получайте больше инсайтов о систематизации бизнеса

Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности

💬 Подписаться на канал