«Змейка наоборот», часть 2: разбор комментариев, исправление багов и добавление босса

Прокомментировать Просмотры: 11

Публикуя первую часть, я откровенно рассчитывал на с десяток сердечек и тишину в обсуждениях — кто станет всерьёз ковыряться в «змейке», наскоро сбитой за пару вечеров? Однако мои прогнозы не оправдались: комментариев оказалось в избытке, и добрая их половина представляла собой обстоятельный разбор полётов. Читатели находили досадные баги, нечестный спавн рептилий прямо под носом у протагониста и проводили параллели то с Pac-Man, то с Asteroids. Перечитывая эти отзывы вновь, я осознал: программный код хоть и функционировал, но далеко не всегда соответствовал изначальной задумке, а исправно работать и отвечать ожиданиям — вещи всё же разные. В общем, я решил пройтись по всему перечню замечаний, а не ограничиваться дежурным лайком.

Что действительно функционировало со сбоями

Начну с самого неловкого момента, который ускользнул от моего внимания при первичном пробегании текста. Пользователь alexs963 прислал фразу гейм-овера: «Вас съела жёлтой змея» вместе со своим рекордом. Сначала я воспринял это как обычный комментарий (мол, человек хвастается результатом), но позже, вчитываясь в отзывы более вдумчиво, споткнулся о грамматическую резь. Проверка показала, что проблема на моей стороне — подлежащее должно стоять в именительном падеже, а не в родительном.

Причина крылась в банальном недосмотре: в структуре с наименованиями оттенков хранились формы вроде yellow: 'жёлтой', которые впоследствии подставлялись в конструкцию Вас съела ${цвет} змея. Подобное выглядело сносно, если мысленно домысливать иное высказывание, но резало слух при буквальном прочтении:

const reasons = {
    green: 'Вас съела зелёная змея',
    yellow: 'Вас съела жёлтая змея',
    red: 'Вас съела красная змея'
};

На исправление ушло ровно три минуты, но подобные ляпы досаднее всего обнаруживать постфактум — ведь с ними сталкивается каждый потерпевший поражение игрок.

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

«мерцает так, будто пытается вызвать припаток эпилепсии»

А также отметил, что динамика в целом превышает классическую, а само яблоко:

«скользит по игровому полю словно по льду»

Погрузившись в отладку, я обнаружил в собственном скрипте забавный архитектурный промах. Очевидно, дело было не в особенностях чьего-то железа: переменная gridSize одновременно управляла плотностью сетки и физическим размером ячеек в пикселях. Из-за этого внутреннее разрешение canvas перманентно расходилось с тем, как браузер масштабировал его посредством CSS. Пиксельный рендеринг в сочетании с дробным коэффициентом масштабирования как раз и провоцируют этот неприятный визуальный дребезг. Я разделил эти параметры: теперь разрешение канваса высчитывается через devicePixelRatio под конкретный контейнер, а количество клеток существует автономно. Заодно разрешилась жалоба на тесноту арены — поскольку размер пикселя больше не привязан жестко к сетке, я без труда расширил игровое пространство без потери четкости. Один точечный фикс закрыл сразу две претензии, что не может не радовать.

Третий сюжет потребовал больше всего времени и сил. gerbert_MX и Metotron0 независимо друг от друга указали на дисбаланс: рептилии беспрепятственно пересекали тела друг друга и, если верить формулировке gerbert_MX:

«формировали замкнутые периметры»

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

const free = perpOptions.find(o =>
    !obstacles.some(ob => ob.x === head.x + o.dx && ob.y === head.y + o.dy)
);
if (free) { this.dx = free.dx; this.dy = free.dy; }

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

if (occupiedByTick[tick].has(this.cellKey(part))) return false;

Попутно, раз уж я взялся за ревизию отзывов, обратил внимание на старое нарекание касательно смартфонов — ещё в самых ранних отзывах упоминалось, что попадать пальцем по элементам управления порой проблематично, а san4ez_gig прямо рекомендовал увеличить их в размерах. Я масштабировал кнопки и увеличил отступы между ними; ранее в альбомной ориентации экранов элементы элементарно обрезались интерфейсом. Очень надеюсь, что мобильная аудитория теперь вздохнёт спокойнее.

Балансировка: избыток динамики со старта

Я и сам в ветке обсуждения первой версии открыто признавал, что при наборе 200–300 очков баланс стремительно рушится: популяция змей возрастает лавинообразно, скорость перманентно высокая, а игровое поле преступно мало. Sfekss сформулировал эту проблему точнее прочих: в оригинальной аркаде темп нарастает плавно по мере поглощения плодов, тогда как моя версия стартовала буквально «с места в карьер».

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

Формулу начисления очков я пересмотрел с подачи сразу двух участников сообщества. gerbert_MX предложил вычислять итоговый результат как произведение «скорости на время», а jouilk23 дельно пояснил суть нововведения:

«это станет стимулом для геймеров идти на риск ради внушительных цифр, вместо того чтобы отсиживаться по углам на минималках»

Возразить здесь нечего, поэтому теперь за каждый прожитый тик копится не банальная единица, а текущий уровень сложности:

this.score += this.currentStageLevel;

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

Анатомия и происхождение яблока

лор яблока
лор яблока

Целая ветка комментариев искренне меня позабавила, хотя, объективно говоря, я сам напросился на подобную реакцию. Wesha иронизировал:

«Заменили бы яблоко на лягушку, а то фрукт, который самостоятельно рассекает по арене…»

— и в этом, если поразмыслить, есть рациональное зерно с точки зрения зоологии, однако проект изначально задумывался вне рамок строгой биологии. Owyn там же, параллельно с претензией к «скользящему по льду» фрукту, добавил:

«прикрутили бы ему хотя бы ноги»

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

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

Новая напасть: подземный червь

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

Фотография врага — червя — и его путь
Фотография врага — червя — и его путь

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

Способности и дополнительные жизни

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

Способности
Способности

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

Крупное нововведение: битва с боссом

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

  1. Жетоны. Королева выпускает на арену рядовых змей, задачей игрока становится сбор трёх золотых жетонов в условиях постоянного маневрирования.

  2. Атака по площади. Зоны поражения подсвечиваются оранжевой заливкой, спустя секунду следует разрушительный удар. Параллельно необходимо успевать подбирать разрастающиеся яблоки — каждое визуально увеличивает персонажа.

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

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

Вместо заключения

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

Конечно, это далеко не все инициативы из комментариев — часть предложений я осознанно отложил в долгий ящик. Все они по-своему интересны, но попытка объять необъятное гарантированно оставила бы вторую (а то и третью) часть в статусе «вечного долгостроя». Приходилось разгребать задачи строго поочередно.

Исходный код проекта опубликован на GitHub, а опробовать игру можно непосредственно в браузере: novelros.github.io/reverse-snake.

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

© 2026 ООО «МТ ФИНАНС»

 

Источник

Поделиться:

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов