Итак, я собрал прототип arpg-проекта Сферамида с теми механиками, о которых писал ранее прошлой в статье Поиграть в браузере/скачать можно здесь:

Ниже некоторые подробности, по игре и приёмам работы с движком Godot.

Особенности игровой структуры и механик

Сферические подземелья

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

Отдельная "комнатка" с коллизией, она же без материала перед экспортом и множество комнат внутри редактора.
Отдельная "комнатка" с коллизией, она же без материала перед экспортом и множество комнат внутри редактора.

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

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

Отдельной .obj модель��ой экспортируется коллизия со стенами, на ней развёртка не нужна, так как она не будет текстурироваться. Внутри Godot модель коллизии добавляется в MeshInstance, для которого вверху основного окна появляются инструменты создания коллайдера на основе полигональной сетки.

Персонаж

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

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

Концентрированное здоровье

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

У шкал здоровья и маны есть параметр концентрации, который падает со временем. Простое зелье не даёт концентрации. Среднее - меньше ресурса, но и некоторое количество концентрации. Сильное - ещё меньше ресурса, но больше концентрации.

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

Параметры персонажа пока не работают и экипируемые предметы не влияют на них
Параметры персонажа пока не работают и экипируемые предметы не влияют на них

Органический инвентарь

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

Камни заклинаний

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

Кнопка действия

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

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

Утилитарная магия

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

Враги

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

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

Сам выстрел не имеет слоя и прочие коллайдеры не отслеживают его
Сам выстрел не имеет слоя и прочие коллайдеры не отслеживают его

Слизни просто путешествуют в том направлении, куда смотрят. Сталкиваясь с препятствиями меняют направление, а коснувшись игрока наносят ему повреждения.

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

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

Скрипты, алгоритмы, логика

Способы связи с объектами

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

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

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

Например, под кнопки бутылок в глобальном скрипте заведён массив uibuttons_arr, куда они заносят ссылки на себя при собственной инициализации. Затем к ним можно обратиться, например к третьей по счёту кнопке - uibuttons_arr[3] (нумерация в массиве идёт с 0, поэтому в данном конкретном случае для удобства записи элементов 7, а первая ссылка всегда остаётся null, то есть uibuttons_arr[0] не используется )
Например, под кнопки бутылок в глобальном скрипте заведён массив uibuttons_arr, куда они заносят ссылки на себя при собственной инициализации. Затем к ним можно обратиться, например к третьей по счёту кнопке - uibuttons_arr[3] (нумерация в массиве идёт с 0, поэтому в данном конкретном случае для удобства записи элементов 7, а первая ссылка всегда остаётся null, то есть uibuttons_arr[0] не используется )

Узлы-обёртки

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

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

Получив событие ext_AfterCollision скрипт Area переадресует его корневому скрипту в виде вызова в нём метода ext_doAction
Получив событие ext_AfterCollision скрипт Area переадресует его корневому скрипту в виде вызова в нём метода ext_doAction

Анимация-таймер

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

Колесо псевдослучайности

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

Суть - имеем заранее созданную последовательность хаотически разбросанных чисел. Например, такой вот массив: [1,0,6,2,8,3,0,2,5,9,7,1,3,9,4,7,8,5,4,6]. К этому массиву прилагается указатель, направленный на конкретную ячейку массива. Каждый раз, когда нам требуется новое случайное "значение" - обращаемся к этому массиву, совершая сдвиг указателя на 1 или другое фиксированное число позиций, в итоге получая значение в той позиции, на которой теперь остановился указатель. Если указатель выходит за пределы массива, то перемещается в его начало.

Указатель остановился на позиции 0
Указатель остановился на позиции 0

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

Указатель остановился на позиции 2, что также означает 30 градусов, 50 процентов и красный цвет в прочих связанных последовательностях
Указатель остановился на позиции 2, что также означает 30 градусов, 50 процентов и красный цвет в прочих связанных последовательностях

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

При создании цепной молнии она поворачивается на "случайный" угол, вызывая глобальный метод с параметрами 2,2 для определения этого угла.
При создании цепной молнии она поворачивается на "случайный" угол, вызывая глобальный метод с параметрами 2,2 для определения этого угла.
Посмотрим на сам вызываемый метод. Первый параметр (prirost) показывает на сколько сместить указатель, второй (what_ret) - из какого именно колеса забрать значение. Колесо rnd_numbers_arr содержит числа, rnd_angles - углы, третье колесо - скорость.
Посмотрим на сам вызываемый метод. Первый параметр (prirost) показывает на сколько сместить указатель, второй (what_ret) - из какого именно колеса забрать значение. Колесо rnd_numbers_arr содержит числа, rnd_angles - углы, третье колесо - скорость.
Если подняться выше по глобальному скрипту до объявления этих массивов, то можно увидеть что содержится в каждом колесе.
Если подняться выше по глобальному скрипту до объявления этих массивов, то можно увидеть что содержится в каждом колесе.

Логика заклинаний

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

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

Защита от множественного срабатывания

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

Часто используемые практики

Создание и импорт моделей

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

Простые модели, без анимаций, я перекидываю в Godot в формате .obj, которые затем следует открывать внутри движка, устанавливая как форму MeshInstance. Нужно убедиться, что у экспортируемой модельки порядок с нормалями, применены модификаторы, есть развёртка (если надо) и экспортируется только она, ничего лишнего и скрытого. Также стоит удалить с модели материал, чтобы собрать его заново уже в Godot. При экспорте также может появиться файл .mtl, он не нужен (тем не менее, более старые версии Godot могут писать об ошибке с .mtl при закидывании .obj файла, но на это можно не обращать внимания).

Анимировать кости в Блендере нужно переключившись в режим Pose Mode, большинству костей хватает анимирования одного только ключа rotate, без scale и location. В процессе анимирования можно подправить веса костей, если возникают заметные искажения на модели. Если анимация должна быть зацикленная, то в конце анимирования, выделив скелет, стоит переключиться в окно Блендера Dope Sheet и там на панели выбрать Key - Interpolation mode - Linear, чтобы анимация шла равномерно, а не замедлялась в начале и конце. Естественно, для зацикленной анимации нужно, чтобы положение костей в начале было таким же, как и в конце (можно даже вынести конечные ключи за пределы итогового трека, на 1 фрейм, чтобы стыковка происходила ещё плавнее). Если не угадали со скоростью анимации в Блендере, то в Godot тоже можно будет управлять скоростью воспроизведения анимации у модельки, через параметр playback_speed.

Импорт анимированного персонажа

Объекты с костной анимацией я экспортирую из Blender в Godot в формате collada (.dae). Это не то, чтобы рекомендуемый вариант, но он работает. Материал с модели также удаляется, а вот модификатор арматуры применять не нужно.

После того как файл перенесён в Godot, нужно щёлкнуть по нему, открыть вкладку Import (слева вверху), в разделе Animation - Storage выбрать пункт "Files (.anim)" и нажать внизу кнопку reimport.

Далее модель добавляется на сцену и нужно открыть её для редактирования (выбрав пункт new inherited). Там, в аниматоре (AnimationPlayer) меняем имя дефолтной анимации на желаемое название анимации и сохраняем эту анимацию как файл .anim. Также сохраняем саму открытую сцену персонажа как файл .tscn - это и будет префаб с моделькой.

Теперь убираем со сцены модельку в формате .dae, и добавляем/используем тот полученный .tscn префаб. Сам файл .dae продолжаем хранить в ресурсах.

Добавление новых анимаций персонажу

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

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

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

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

Полезное

Фишки среды разработки

В Godot можно нажать Ctrl и кликнуть по строке кода с вызовом функции, чтобы переместиться в то место, где находится эта функция. Таким же образом, кликая по переменным можно перемещаться в место, где эта переменная объявляется. Выделенный код можно двигать вверх-вниз стрелками клавиатуры при зажатом Alt. Чтобы закомментировать или раскомментировать несколько выделенных строк сразу нужно нажать сочетание Ctrl + K. Кнопки Home и End перемещают курсор ввода на начало или конец строки кода. Если нажать Ctrl + C, не выделяя ничего, то в буфер обмена копируется вся строка на которой сейчас находится курсор ввода. В то время как кнопка Tab добавляет символ табуляции, сочетание Shift + Tab удаляет символ табуляции. Tab и Shift + Tab также работают при выделении нескольких строк сразу - добавляя всем шаг табуляции или удаляя его.

Если щелкнуть мышкой левее строчек кода, у самой границы окна, то появится красная точка - breakpoint. Также можно делать это кнопкой F9. Выше окна кода есть пункт "Go To", где во вкладках Bookmarks и Breakpoints находятся инструменты для работы с подобными отметками - поставить, снять, перейти к следующей.

Эффект просвечивания

Для того, чтобы силуэт героя подсвечивался, когда его перекрывает препятствие, нужно сделать вот что: у базового материала героя приоритет рендера сменить с 0 на 1, а в next pass добавляется второй материал с нужным цветом и флагами unshaded и no depth test.

Спасибо за внимание.