Александр Лысенко
Синдром Утенка
-
Вступление
Игровой джэм (от англ. game jam) - это событие в течение нескольких дней, на котором разработчики создают игры на определенную тему. Само слово “джэм” относится к музыкальной импровизации нескольких человек, когда они подыгрывают на музыкальных инструментах между собой.[1] Программисты, дизайнеры, композиторы, писатели и художники могут собраться в команду и создать игру подобным способом. Есть игры с единственным разработчиком. Люди учатся, закрепляют навыки через практику, и смотрят произведения друг друга.
В мае 2024 года проходил джэм Pixel Game Jam 2024 (с 11 до 20 мая), основной темой которого было создание игры с пиксельным оформлением.[2] К началу события, девять тысяч человек со всего мира присоединились к списку участников. Мероприятие должно было длиться десять дней, в командах размером до трех человек, с разрешением на любые движки или технологии. В этой статье, я опишу общий ход создания игры, которая получила название “Синдром Утенка”.
День 1. Планирование.
В моем распоряжении имелось десять дней, и первый из них был проведен за размышлениями о том, какую игру я хочу сделать.
Была раскрыта дополнительная тема игрового джэма — “Аква”, и все, что связано с водой.[3] Организаторы мероприятия намекнули на солнечный пляж и море. Практически же игра могла быть и про лед, рыбу, даже сантехнику. Сложность была не в том, чтобы добавить водные эффекты и изображения, а в том, чтобы вода стала сущностью игрового процесса. Например, в игре, в которой нужно строить песчаные замки во время прилива, темой является не вода, а скорее песок. Или, в другой игре, в которой дождь только едва виден, или волны только немного слышны, вода также не будет основной темой.
В этот день я рассмотрел разные варианты, но не приступал к работе.
День 2. Пиксельный холст и двухмерная вода.
Я решил написать свою игру на чистом JavaScript, чтобы она запускалась в браузере. На удивление, сделать пикселизованный холст средствами веб-разработчика достаточно просто:
<canvas id="canvas" width="128px" height="72px"></canvas>
<style>
#canvas {
image-rendering: pixelated;
}</style>
Так как игровое поле состояло из относительно небольшого количества
пикселей (128*72=9216
), я был искушен добавить в игру
динамическую поверхность воды. В тот момент, у меня не было
представления, будет это маленькая лужа или большой водоем. Все, что я
хотел, это видеть бегущие кругами волны после щелчка, и чтобы это
рассчитывались во время исполнения игры. В интернете нашелся алгоритм
двумерной воды, упомянутый в нескольких источниках.[4]
Похоже, что никто не знает, кто его придумал; и он передавался от одного
поколения программистов к другому.

Этот алгоритм стал основой моей игры, и я подробнее объясню его принцип. Одномерная реализация (которая легче для понимания) выглядит следующим образом:
for (let i=1; i<buffer1.length-1; i++){
= (buffer1[i-1] + buffer1[i+1])/2 - buffer2[i];
buffer2[i] = buffer2[i] * 0.5;
buffer2[i]
}
for (let i=0; i<buffer1.length; i++)
, buffer2[i]] = [buffer2[i], buffer1[i]]; [buffer1[i]
В массиве содержатся высоты волн для каждого пикселя на холсте. Когда игрок щелкает по холсту, в соответствующий элемент массива добавляется значение, которое волнует воду. Высоты волн могут быть преобразованы в яркость цвета, прозрачность, или другие графические эффекты. Программа пробегает по каждому кусочку воды и выполняет три главные операции:
- Сглаживание: Когда состояние воды обновляется, высота волны на данном пикселе считается как среднее арифметическое от высот волн на его соседних пикселях. Высокая волна падает, сглаживается и размывается. Таким образом волны распространяются, и бегут от высоких точек к низким (и наоборот).
- Затухание: Каждое значение в массиве умножается на коэффициент меньше единицы (например, 0.95), потому что мы ожидаем, что волны затухают с течением времени. Без эффекта затухания, новые добавленные волны копятся, и повышают общий уровень воды. С эффектом затухания, высота волн вскоре возвращается к нулю.
- Обращение: После сглаживания, из новой высоты волны на данном пикселе, переданной от соседей, вычитается предыдущая высота волны. Когда мы учитываем и обращаем предыдущую (затухающую) высоту волны, волны колеблются как настоящая поверхность воды. В нескольких словах, без этой операции “волна” на двумерном поле выглядит как яркая точка, которая плавно растекается до блеклого круга, после чего исчезает.
Единственное отличие двумерной реализации в том, что сглаживание происходит по двум осям. Я изменил силу с которой волны бегут вверх и вниз; благодаря этому в игре они выглядят овальными и возникает ощущение перспективы. Это и стало результатом второго дня разработки.
День 3. Алгоритм поиска пути.

На холсте появился персонаж, который пока выглядел как квадрат. Я не планировал добавлять препятствия, поэтому алгоритм передвижения мог быть также прост. Чтобы персонаж передвигался из одной точки в другую, я использовал известный алгоритм Брезенхэма.[5] Программа добавляет в массив все точки, которые лежат на прямой между игроком и местом щелчка. Игрок последовательно проходит эти точки, пока не окажется рядом с целью.
День 4. Главный персонаж и название игры.
Пришло время определиться с главным персонажем и нарисовать его спрайты. В этот день я окончательно решил, что им станет утенок, а игра получит название “Синдром Утенка”. Мне показалось, что утенок выражает водную тему особенно хорошо, и вышла бы интересная игра. В природе, утята привязываются к первой “матери”, которую видят после вылупления, и повсюду за ней следуют.[6] Утенок плыл за курсором мыши, и это стало причиной для такого названия.

Я воспользовался программой Krita, чтобы нарисовать утенка в восьми направлениях. Изображения должны были меняться в зависимости от направления к щелчку мыши. Для этого понадобилась функция арктангенс2, которая возвращает угловую меру между осью абсцисс и другим лучом из начала координат.[7] В моем случае, основанием луча было положение утенка, а другой точкой – координаты щелчка. Я забегаю вперед, но за четвертый и пятый дни я также поменял свое мнение на счет единиц измерения углов. Мое первое решение, по привычке, полагалось на излишние градусные преобразования, но в итоге я принял удобство радиан.
День 5. Конечный автомат состояний поведения персонажа.

Ранее, как только игрок нажимал мышкой на воду, утенок совершал поворот и рывок в ту сторону моментально. Чтобы поведение игрового персонажа выглядело более реалистично, утенок стал поворачиваться к цели плавно. Это стало возможным после разделения поведения персонажа на три состояния: Бездействует, Поворачивает, и Плывет.[8]
В состоянии бездействия утенок качался на волнах и медленно уплывал в сторону. После щелчка утенок переключался в состояние поворота и продолжал крутиться на месте, пока цель не окажется на его линии взгляда. Имея цель впереди себя, утенок к ней плыл. Находясь рядом с целью, утенок возвращался в состояние бездействия. В этот же день утенок научился, быстрее ли поворачиваться по часовой стрелке, или против часовой стрелки.
День 6. Каустики в Blender и границы.

На шестой день я смоделировал водную емкость в Blender, и добавил солнечные лучи на дно бассейна. Эти меняющиеся формы называются каустиками, и возникают при преломлении света на волнующейся поверхности воды.[9] В интернете есть много видео, объясняющих как воссоздать такой эффект с помощью диаграммы Вороного.[10] Потребовалось время на поиск метода, чтобы сделать бесшовную повторяющуюся анимацию.[11] В моей игре, десять изображений с каустиками меняются со скоростью тридцати кадров в секунду, что и создает иллюзию продолжительного движения воды.
Я нашел функции двух прямых по координатам, выше которых находится место вне водоема, и где утенок плавать не может. На дне бассейна видна круглая тень, подготовленная для мяча в следующих версиях игры.
День 7. Звуковые эффекты.
К седьмому дню игра оставалась беззвучна. У меня было несколько попыток записать отрывок в стиле лаунж или босса-нова, которые иногда ассоциируются с летней пляжной музыкой. Это заняло много времени, хотя даже и не являлось основной частью игрового дизайна. В результате я отказался от фоновой музыки.
Первым звуком в игре стал звук утенка, который я с трудом нашел в онлайн-библиотеке с бесплатными звуковыми эффектами.[12] К сожалению, в ней был только визг утенка, который скорее всего потерялся. Когда утята теряют из вида своих родителей, они тревожатся и начинают звать на помощь, что сильно отличается от их расслабленного щебета. Вторым звуковым эффектом стал всплеск, который я записал в тарелке с водой в ванной; хотя он появился в игре только на десятый день.
День 8-9.
Почти без прогресса.
День 10. Завершение проекта.

Время на разработку подходило к концу, но многие части моего проекта оставались недоделанными. Я провел последние часы рисуя дополнительные спрайты и тени. Утенок стал поворачивать на ходу, и внешне совершал больше движений, когда плыл к цели. Я поспешил внести последние изменения в код. На фоне появилась зеленая трава, а в воде — мячик с моими инициалами, когда-нибудь на память.
Когда время приема заявок истекло, было отправлено более 700 проектов от других разработчиков![13] Некоторые полностью посвященные воде, а некоторые с водой совсем не связанные. По сравнению со многими другими работами, моя игра имела очевидные недостатки, так как в ней не было уровней, или условий победы и поражения. Все же, я получил много положительных отзывов и идей, как игру можно улучшить. Разработка игры принесла мне радость, потому что приятно видеть, как из ничего получается что-то. Это не совсем правда, так как я имел предыдущий опыт, и заметки, оставленные другими энтузиастами в интернете.
Обновление. Улучшение скорости отрисовки.
Игра работала на моем настольном компьютере, но я заметил визуальные
задержки на бюджетных мобильных телефонах. Существуют рекомендации для
улучшения скорости отрисовки на холсте.[14]
Вместо того, чтобы рисовать пиксели воды один за другим
< 9216
раз, я использовал другой метод, в котором
компьютер сначала меняет данные пикселей, и после рисует все пиксели за
раз.[15] Браузеры также предоставляют
функцию requestAnimationFrame()
для синхронизации запросов
отрисовки с частотой обновления экрана.[16]