Добавить в корзинуПозвонить
Найти в Дзене
ZDG

Дневник разработки игры Pengu5 20.04.2026

С прошлого раза я сделал существенный прогресс в разработке игры Pengu5, и это меня радует – обещание двигать её вперёд пока выполняется. Однако что это за прогресс? Абсолютно скучная рутина, которая практически никак не отражается на результате, но должна быть сделана. Так что я даже не буду описывать никакой код, а просто поделюсь тем, что входит в разработку простой, как три копейки, игры. Я использую специально нарисованный шрифт, и мне пришлось пройтись по каждой букве и вручную модифицировать её размеры так, чтобы они были целыми числами (изначальны буквы нарисованы в векторной графике). Но это не самое сложное. Я уже поднимал тему кернинга символов в другой статье: И сейчас решил сделать его по-настоящему. Для каждой возможной пары символов нужно вручную, полагаясь на визуальную оценку, задать расстояние между символами. Так как у меня в алфавите пока всего 36 символов, я делал так: Итого я затратил несколько часов, чтобы задать отступы для 36*36=1296 комбинаций символов. Но бли
Оглавление

С прошлого раза я сделал существенный прогресс в разработке игры Pengu5, и это меня радует – обещание двигать её вперёд пока выполняется.

Однако что это за прогресс? Абсолютно скучная рутина, которая практически никак не отражается на результате, но должна быть сделана.

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

Кернинг шрифта

Я использую специально нарисованный шрифт, и мне пришлось пройтись по каждой букве и вручную модифицировать её размеры так, чтобы они были целыми числами (изначальны буквы нарисованы в векторной графике).

Но это не самое сложное. Я уже поднимал тему кернинга символов в другой статье:

И сейчас решил сделать его по-настоящему.

Для каждой возможной пары символов нужно вручную, полагаясь на визуальную оценку, задать расстояние между символами.

Так как у меня в алфавите пока всего 36 символов, я делал так:

  • Берём букву "A".
  • В графическом редакторе перетаскиваем по очереди все символы из набора к букве "A", смотрим, какой отступ надо сделать.
  • Переключаемся в IDE и вписываем этот отступ в массив.
  • Берём букву "B", повторяем все действия, и т.д.

Итого я затратил несколько часов, чтобы задать отступы для 36*36=1296 комбинаций символов.

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

Загвоздка в том, что все пары символов надо проверять визуально, они очень своеобразные. Поэтому процесс никак не автоматизировать – надо перетаскивать символ, ставить рядом с другим и смотреть, что получается. Плюс-минус один пиксел играет роль.

В отчаянии я обратился к Дипсику – мол, как тут ускорить процесс? И он назвал мне совершенно очевидное решение, о котором я почему-то не подумал. Нужно просто написать свой редактор символов, где и двигать их.

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

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

Теперь процесс хоть и не полностью автоматизирован, но стал, наверное, в 100 раз быстрее. Кроме того, мне даже не надо задавать кернинги абсолютно для всех сочетаний. В игре очень ограниченное количество текста, так что я могу взять только реально существующие строки, вывести их, и поправить только те сочетания, которые нуждаются в поправках. А все остальные сочетания просто никогда не появятся на экране.

-2

Далее, возникает вопрос, если мы храним значения отступов для каждой пары символов, значит, размер такого массива будет квадратом от количества символов? То есть, для 256 символов массив отступов будет занимать 64 килобайта?

Да, это с одной стороны много, а с другой – совсем мало, при нынешних объёмах памяти. Тем более символов у меня будет даже не 256, а максимум 80. Так что беспокоиться не о чем.

Но если это дело оптимизировать по памяти, то достаточно будет хранить только нестандартные пары в виде карты [пара => значение].

Атлас шрифта

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

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

Это не так уж сложно и долго, по сравнению с остальным объёмом работы, но раз пошла такая пьянка, я решил автоматизировать и это дело. Ведь мне в будущем понадобится переделывать символы или дорисовывать новые.

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

Затем я удаляю символы и оставляю только прямоугольники. Сохраняю эту картинку.

Затем я попросил Дипсика написать мне скрипт на PHP, который сканирует эту картинку, находит цветные прямоугольники и определяет их позиции и размеры. Что Дипсик успешно и сделал.

Почему на PHP? Потому что у меня установлен PHP для веб-разработки, и я использую его как скриптовый язык для командной строки, когда требуется какая-то вспомогательная утилита.

Ад коллбэков

В сообществе JS-разработчиков есть выражение Callback Hell, или ад коллбэков. Дело в том, что JS это событийно-ориентированный язык, поэтому на любое асинхронное действие, такое как загрузка картинки или обработка таймера, нужно писать свой коллбэк (иначе говоря, функцию, которая обрабатывает это событие).

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

Например, в игре происходит так:

Загрузка основных ресурсов (каждый ресурс грузится асинхронно и порождает своё событие), затем событие, что всё загружено, затем загрузка игрового уровня и событие, что он загружен, и т.д.

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

-3

Современные практики используют async / await, но мне вот лично не хочется их использовать, потому что по сути это лишь синтаксический сахар, который прячет те же самые коллбэки от нашего понимания. Я хочу решать проблемы средствами простейшего, посконного JS.

В итоге я просто разработал сценарии загрузки. У игры их несколько – собственно первичная загрузка, загрузка меню, загрузка уровня, и т.д. Для каждого случая я делаю свой, отдельный класс сценария. Сценарий просто икапсулирует всю грязную работу: он знает, какие ресурсы в каком порядке загружать, куда обращаться после загрузки и т.д. Сценарий содержит коллбэки в виде своих собственных функций, сам их назначает, и сам их обрабатывает.

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

Модбоксы

Модбоксы я ранее описывал в этом материале:

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

Например, модбокс "платформа" двигает вместе с собой все добавленные в него объекты, а модбокс "гравитация" заставляет все объекты падать вниз.

Если делать реалистичную физическую симуляцию, то объект, стоящий на платформе, падать вниз не будет, хотя на него и действует гравитация. Просто физический движок будет постоянно сталкивать объект и платформу. И вы наверняка наблюдали связанные с этим глюки.

У меня взаимодействия логически разделяются. Когда объект добавляется в модбокс "платформа", он удаляется из модбокса "гравитация", и гравитация перестаёт на него действовать, она в данном случае просто не нужна. Когда же объект покидает платформу (например, пингвин подпрыгивает), он удаляется из модбокса "платформа" (платформа перестаёт двигать его вместе с собой) и добавляется в модбокс "гравитация", и гравитация снова начинает на него действовать.

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

Но есть нюанс

Когда пингвин падает на плавающую льдину, происходит следующее:

  • Льдине придаётся импульс движения вниз (она плавно погружается и снова всплывает). У пингвина сбрасываются анимации, чтобы он просто стоял.

Когда льдина с пингвином движется, происходит следующее:

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

Когда пингвин подпрыгивает с льдины, происходит следующее:

  • Льдине опять придаётся импульс движения вниз от толчка. У пингвина включается анимация полёта.

Зная, что речь идёт о конкретной льдине и конкретном пингвине, всё это легко реализовать внутри модбокса.

-4

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

  • Когда пингвин запрыгивает на льдинку, импульс движения вниз ей не придаётся – она просто проседает на фиксированное расстояние вниз.
  • Когда пигвин находится на льдинке, он не стоит, а скользит по ней непрерывно, пока не упадёт (такого вообще нет в случае с основной льдиной, с которой можно только прыгнуть, а не упасть).

Итого, имеем функционально один и тот же модбокс, но конкретные детали обработки объекта у него должны быть разные. Что делать?

И ведь это речь только о пингвине. А что если на льдину падает, допустим, ракушка? Она должна просто лежать, никуда не скользя, и у неё нет анимации, которую нужно переключать. И т.д.

Возможно (это запоздалая мысль), я мог бы просто сделать две разновидности платформенных модбоксов для каждого варианта, но пока получилось так:

Контексты

Я сделал новую сущность – контекст для модбокса. Сам модбокс выполняет механическую обработку атрибутов объекта – например, перемещает его вместе с собой.

А вот контекст решает, что делать при добавлении и удалении объекта, а также при его штатной обработке. Таким образом, я могу написать класс контекста, который включает различные анимации пингвина и придаёт импульс движения платформе и выполняет проскальзывания по инерции, и прикрепить этот контекст конкретно к плавающей льдине. А к летящей льдинке прикрепить другой контекст.

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

При создании модбокса можно подсунуть для него любой нужный контекст и таким образом настраивать любые модификации объектов при добавлении и удалении.

Однако же, что будет, если на платформу упадёт ракушка? Контекст попытается сбросить у неё анимацию, но у ракушки нет анимации, так как она не пингвин. Возникнет ошибка.

В данном случае используется один контекст на весь модбокс, так как я знаю, что пока на платформу будет падать только пингвин. Для расширения можно сделать контекст, который различает ракушки и пингвинов и поступает соответственно, либо каждый добавляемый объект может добавляться вместе с собственным контекстом.

Мне это пока не нужно, а пути решения уже известны.

Но есть нюанс

Все модбоксы должны обрабатываться в строго определённом порядке. Например, есть модбокс, который ограничивает движение объекта, чтобы он не вышел за пределы экрана. Таким образом, сначала отрабатывают все модбоксы, которые так или иначе двигают объект, а последним должен отработать тот, который ограничивает его перемещение.

И тут я внезапно столкнулся с неучтённым фактором. Платформа-льдина находится у меня на определённом месте в списке модбоксов. Но летящие в воздухе льдинки появляются и исчезают динамически! То есть, каждая из них это модбокс, но куда его добавлять, в какое место списка? И как потом удалять? Этот момент я совершенно не учёл при первичной разработке.

В результате я сделал такую вещь, как "группа модбоксов". Основной список модбоксов остаётся фиксированным, но вместо модбокса-платформы я добавляю специальный модбокс, который является группой модбоксов-платформ. Далее, когда у меня возникает новая платформа, я добавляю её в группу платформ. Таким образом, основной список модбоксов не меняется, а группа платформ обрабатывается как модбокс со своим внутренним списком объектов, со штатными добавлениями и удалениями.

Остальное

Помимо этого, я переделал кое-какую графику, сделал анимацию новых элементов меню, полностью переработал и упростил систему событий, переработал и упростил классы графических примитивов, переработал коллайдер объектов, добавил работу со звуком и многое другое.

Всё это было наследием более чем 10 лет предыдущей разработки, так что действительно, исправлять труднее, чем писать с нуля. Практически ни один файл не остался нетронутым.

В процессе я составил список пунктов, которые необходимо завершить для выпуска демо-версии. Первоначально список был из 7 пунктов, но в процессе доработки каждого пункта появлялись новые, и теперь он состоит из 20 пунктов.