Куда летает махолёт

О разработке проекта на Godot для 72-х часового игрового джема.

Куда летает махолёт

Речь пойдёт о прошлогоднем онлайновом осеннем Siberian Game Jam, где требовалась за трое суток сделать игру (на тему "Закон Мёрфи", интерпретируемую автором как угодно). Одно из дополнительных требований - делать весь контент именно своими руками (картинки, текстуры, модели, аудио и так далее).

Игровой движок можно было брать любой. Я остановился на Godot (версии 3.3.3), выбирая между ним, Unigine и PlayCanvas'ом. Второй медленнее в разработке, а третий к тому моменту не вовремя обновился, поломав привычный пайплайн.

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

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

В списке вероятных тем для конкурса не заметил такой, которая сильно шла бы вразрез с задуманным, поэтому продолжил прорабатывать это направление. Таким образом, к старту конкурса я уже определился с тем, что это будет игра про махолёт с какой-то dizzy-like (а Dizzy - это прадедушка этих ваших метроидваний, ну и не только их) механикой сбора/применения предметов. Заодно прикинул, что обычного вида островки я, наверное, не успею сделать, поэтому заранее замоделил модульные шестиугольные колонны, чтобы собирать из них островки-кластеры, а при необходимости придать им вид башенок или что-то в этом роде (если выпала бы тема "Ритмы города").

Вот такая игра получилась в итоге:

Поиграть в тот билд можно скачав архив для Windows на страничке itch.io (вес файла около 20 Мб):

И, вернёмся обратно к началу.

Тема конкурса ("Закон Мёрфи") выпала ни о чём, ещё и оправдывающая любые возможные баги (всю критику по вопросам темообразования организаторы выслушали с умными глазами и продолжили делать по своему - через год имеем очередную пустую трешовую тему "Есть один нюанс...").

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

Таким образом я решил включить в Махолёт такой привет из "Вангеров", как моменты с внезапно выпадающими грузами. А то привыкли, понимаешь, в современных играх, что в инвентаре всё всегда в целости сохранности, да на своих местах. Почему бы не нарушить эту концепцию... Так так, а персонажами тогда будут говорящие головы, к ним можно будет залетать в гости... А почему бы ещё грузам, собственно, не портиться по пути. Или как-то негативно/позитивно влиять друг на друга в инвентаре. Или... в общем, мысли уже текли в направлении развития механик.

Обдумывая это всё дотекстурил колонны, замоделил головы, стал накидывать в игре функционал активных точек, где можно поговорить с персонажами (на пустышках и примитивах). Отрендерил в Blender самодельную карту освещённости (HDRI). Записал звук для взмаха крыла на телефоне. Аудио получилось в формате .m4a, но, как оказалось, сконвертировать его в понятный движку .ogg умеет установленный Vlc плеер и ничего дополнительного искать не нужно. Переимпортировал файл в Godot, чтобы убрать неотключаемое зацикливание, выставляемое для ogg по умолчанию на этапе импорта. Далее привязал проигрывание звука к анимации временных заглушек для крыльев, чтобы синхронизировать его со взмахами.

Затекстуренные элементы уровня
Затекстуренные элементы уровня
Временная заготовка летательного аппарата
Временная заготовка летательного аппарата

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

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

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

Далее, я хотел отделить предметы, которые будут лежать на своих местах на старте и ничего не делать в update от тех, которые постепенно полетят вниз, когда будут выброшены махолётом. Поэтому для удобства завёл сцену с пустышкой-спавнером, скрипт которой будет на старте игры прикреплять статический предмет на свои координаты в мире. А махолёт, по задумке, должен был создавать (когда выбрасывает предмет) другую сцену-пустышку, которая подгрузит предмет внутрь себя и далее будет падает вниз вместе с ним. Правда времени на реализацию падения предметов не хватило, поэтому они просто спавнятся в мире на старте, а при выбрасывании висят в воздухе (да и нужна ли им эта гравитация в принципе - отдельный вопрос).

1 - Универсальная сцена, с визуалом всех предметов, 2 - Та же сцена с выставленным конкретным предметом, 3 - Визуал предмета внутри сцены-оболочки, отслеживающей столкновения. Всё вместе - активный предмет, 4 - Сцена-пустышка, создающая в мире активный предмет в своих координатах, 5 - Сцена с эффектом гравитации, внутри неё активный предмет.
1 - Универсальная сцена, с визуалом всех предметов, 2 - Та же сцена с выставленным конкретным предметом, 3 - Визуал предмета внутри сцены-оболочки, отслеживающей столкновения. Всё вместе - активный предмет, 4 - Сцена-пустышка, создающая в мире активный предмет в своих координатах, 5 - Сцена с эффектом гравитации, внутри неё активный предмет.

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

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

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

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

В конце игры желательно было сделать рестарт, чтобы игрок мог нормально начать игру по новой, не перезапуская приложение. Для этого потребовалось дополнительно продумать, каким образом это будет происходить. Я остановился на том, что создал специальный слой (сцену), содержащий в себе спавнеры стартовых предметов. А выбрасываемые из махолёта предметы крепил уже именно к этому слою. В конце игры происходит удаление этого слоя (вместе с которым удобно удалятся и все предметы, выброшенные во время игровой сессии) и загружается его новая чистая копия, со стартовой расстановкой. Прочие игровые объекты - махолёт, головы, "островки", менюшки интерфейса - при рестарте остаются нетронутыми, однако им выставляются стартовые настройки внутри скриптов. То есть головы возвращатся к своим первым фразам, махолёт возвращается в стартовую позицию и обнуляет свои предметы, скрываются/показываются нужные менюшки.

Не во всех играх рестарт так уж жизненно необходим, особенно если можно легко выйти или перезагрузить страницу (в случае бразуерной игры), но по всей концепции выходило, что предметы будут теряться и ломаться, следовательно нужно сразу предусмотреть либо какое-то их реинкарнирование, либо вариант отката к началу, либо ещё какие альтернативы. Поэтому есть рестарт, а для текущего "прохождения" предметы и не требуются.

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

Для тестов я завёл некоторые вспомогательные кнопки - "создать случайный предмет", "увеличенная скорость" и "конец игры". Главное не забывать убирать подобные штуки из релиза.

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

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

Для того, чтобы предотвратить массовый триггер (если вдруг на махолёте окажется 2 или 3 одинаковых типа предмета сразу), второй предмет срабатывает на "лимит ревизий +1", а третий на "лимит ревизий +2". То есть, например, у нас на борту 2 бочки и 1 ящик. Бочки выпадают, допустим, после 100 ревизий, но если бочка лежит в слоте 1 или 3, то она выпадет на ревизии 101 или 102, соответственно. При выпадении каждой бочки связанные с этим типом предмета счётчики ревизий обнуляются, то есть следующая бочка выпадет ещё через 100+ проверок. Таким образом две бочки не выпадут одновременно и пока на борту есть одна, то для другой время фактически не крутится.

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

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

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

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

Перевозимые предметы, кстати, не успел замоделить и затекстурить, но я на что-то такое и рассчитывал, планируя в подобном случае обозначить их примитивами и выделить на предметы пару отдельных однотонных материалов. Так и произошло.

Хинт - в Godot среди формы мешей нету тора, но есть CSG объект в форме тора, поэтому для кольцеобразных объектов можно использовать его. Ну и в целом из самих CSG можно набросать какие-то произвольные тестовые формы, обойдясь без 3д-пакета, главное их не двигать относительно друг друга в реальном времени во время игры, чтобы не считать булевы операции по новой. Хотя и это можно, если сильно надо - одна из моих игр построена на этом эффекте. Если контактируют немного объектов, они относительно простые и общая площадь соприкосновения не такая большая, то потеря производительности от движущихся CSG не так существенна и можно это как-то использовать.

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

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

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

Кучу попутно придумавшихся предметов я убрал подальше, выбрав для реализации только 3 квестовых, ну и для разнообразия добавил ещё 3 "мусорных" предмета, которые просто можно потаскать. Квестовые предметы тоже мало что дают, кроме того, что на них отреагируют получатели. Но я собирался добавить некоторым из них эффекты, если останется время, и это получилось сделать. Ещё я выбросил вроде бы одну из основных планируемых механик - возможность применить правый предмет к какой-то активной точке (как в Dizzy). Просто у меня уже была возможность дать предмет голове, через кнопку в интерфейсе общения с ней, и решил, что этого в принципе будет пока достаточно. Также под нож пошёл механизм выдачи предметов махолёту самими головами - они должны были уметь бросать их на какие-нибудь платформочки, неподалёку от себя. Зато лишний балл в раскрытие темы конкурса - почему, действительно, персонажи должны что-то давать за выполнение заданий? В конце концов у них может быть совсем иной взгляд на ваши взаимоотношения или банальный склероз, или ещё что.

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

Впоследствии я отрендерил в Блендере вот такой мини-ролик по мотивам этого проекта:

горизонты сюрреализма

И немного улучшал какие-то общие моменты в прототипе, не собирая пока новых игровых билдов:

например, добавил динамический зум камеры, дневной вариант освещения и возможность применять ключевые предметы к активным объектам
1010
Начать дискуссию