Игра Relight создана в рамках недавнего Go Godot Jam 2, примерно за 8 дней. Игровой процесс заключается в том, что нужно переключать измерения, для того чтобы проходить уровни. В одном мире нужно ловить искры, в другом накапливать заряд из сфер, а в третьем собирать труднодоступные ресурсы или находить точки телепортации. Ниже можно посмотреть видео с фрагментами геймплея:

А теперь посмотрим, как проект устроен внутри:

первые наброски
первые наброски

Сфера и базовые элементы

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

Таким образом персонаж двигается вперёд, локально вращаясь по оси X, или поворачивается в стороны, вращаясь уже по оси Z. Различные собираемые сущности и противники тоже расставляются по планете через поворачивание на различные углы.

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

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

Уровни

Для некоторой оптимизации я решил совмещать на одной модели планеты по два уровня. По крайней мере для начальных. Таким образом всего вышло две планетки (одна для уровней 1-2, другая для 3-4), а уровни 5 и 6 переиспользуют каждую из них целиком.

Планеты моделировал в Blender с последующей конвертацией в .obj для закидывания в Godot. Меши для коллизий экспортировал отдельно (без развёртки, которая им не нужна) и уже в Godot делал из этих мешей сетку коллизий.

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

рейкасты отмечены синим
рейкасты отмечены синим

Игромеханика

Базово в игре используется уже седьмая по счёту итерация идеи совмещения нескольких простых механик в одной общей концепции, завязанной на единую шкалу, смысл которой меняется в разных измерениях. Первый прототип был написан ещё на Blitz3d. Затем более отполированная flash-версия, уже в 2д. Видеомоменты по ним есть тут: Blitz3d , Flash

В разных измерениях смысл шкалы меняется - здоровье героя, его вес или время.
В разных измерениях смысл шкалы меняется - здоровье героя, его вес или время.

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

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

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

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

некоторые прошлые итерации
некоторые прошлые итерации
нижнее измерение в Relight
нижнее измерение в Relight

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

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

Интеллект врагов

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

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

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

Интерфейс

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

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

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

Текстуры и материалы

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

основной персонаж среднего измерения
основной персонаж среднего измерения
подготовка анимации после завершения текстурирования
подготовка анимации после завершения текстурирования
нематериальный персонаж для нижнего измерения
нематериальный персонаж для нижнего измерения

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

как видно, в каждом из этих атласов ещё и место осталось
как видно, в каждом из этих атласов ещё и место осталось

Также понадобилось сделать свои plane, которые содержали бы уникальные развёртки ссылающиеся на нужную область атласа. Важный момент - для частиц эти plane нужно было повернуть специальным образом перед экспортом из Blender, чтобы игровой движок правильно отображал их в режиме билбордов. В самом же движке Cull Mode у материала для частиц должен быть переведён в Disabled (как будто материал двухсторонний), так как если оставить Back, то есть вероятность, что на каком-нибудь железе частицы будут билбордится не той стороной и их не будет видно. Скорее всего такого не случится, но на ноубуке с немного кривоватым драйвером я такой нюанс отловил во время тестов.

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

Слева белый меш задающий области травы. Справа - мультимеш с травой на его основе.
Слева белый меш задающий области травы. Справа - мультимеш с травой на его основе.

Так как в игре три разных измерения, то каждому нужна была своя карта освещённости (HDRI). Godot поддерживает формат .exr, поэтому для уменьшения размера я рендерил hdri-изображения в Blender и сохранял в .exr. В целом они всё-равно много весили по сравнению с прочим игровым контентом, поэтому для web-версии перерендерил сохранённые сцены заново, с качеством пониже (выставив размер 1024 на 512 вместо 2048 на 1024).

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

Слева планета 3-4, справа 1-2
Слева планета 3-4, справа 1-2
процесс сворачивания элементов развёртки
процесс сворачивания элементов развёртки

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

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

По отдельному материалу пошло на каждого из двух основных персонажей, а элементы третьего, расположены на текстуре одного из них. А на текстуре другого расположилась фрагменты вражеских "вертолётиков".

Скрипты

Вся основная логика крутится в главном скрипте сцены (MainScript), который никуда не девается между уровнями. Сами уровни подгружаются и выгружаются по ходу дела.

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

Прочие объекты обращаются в главный скрипт через глобальную ссылку на него, которую он выкладывает на самом старте в синглтон с глобальными переменными. Таким образом, собранная искра вызывает метод "Собрать искру" в теле MainScript, а он уже сам осуществит обновление шкалы, проигрывание нужного звука и так далее.

Собираемые сферы содержат скрипт Bonus, обрабатывающий столкновение с игроком. Скрипт sfxBonus висит на префабе, который остаётся после сбора сферы и когда запущенный в этом префабе аниматор остановится, то посылает данному скрипту сигнал, обрабатывая который префаб самоуничтожиться. Скрипт BonusTime управляет теми сферами, которые игрок должен собирать в третьем измерении (так как это обрабатывается немного иначе).

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

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

Итог

Ну вот, собственно, и всё. Спасибо за внимание.

Комментарии (0)