Итак, я собрал прототип 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] не используется )](https://habrastorage.org/r/w1560/getpro/habr/upload_files/dbb/7e1/779/dbb7e17797e3beb8f0a0930eb52d16bd.jpg)
Узлы-обёртки
В большинстве случаев удобнее крепить отдельные специфические элементы (модель, спрайт, кнопка, текстовое поле) к простому стандартному узлу, который будет выступать в качестве контейнера. Это очень пригодится, например, при создании анимаций средствами движка - вместо того, чтобы непосредственно анимировать какую-то модель (MeshInstance или префаб), анимируется узел-контейнер модели. Таким образом, если потребуется сменить модельки, то можно будет не переделывать анимацию заново, а подкорректировать уже имеющуюся. То есть вместо конкретной анимации "молоток ударяет по гвоздю", делается анимация вида "пустышка ударяет по пустышке", где в первую вложен "молоток", а во вторую "гвоздь". И позднее можно вложить в них другие модели, например, "топор" и "полено" (или заменить "молоток" и "гвоздь" высокодетализированными версиями), без пересоздания анимационного трека заново.
Что касается префабов - стоит продумать, какой именно узел будет "лицом" префаба, то есть корнем его иерархии. Лучше всего в корень помещать пустышку, но бывают исключения. Например, у 3д объектов с коллайдерами может иметь смысл вынести в корень саму Area с коллизией, чтобы вешать основной скрипт префаба сразу на неё. В противном случае получается, что если с коллайдером внутри префаба что-то столкнулось и вызывает в нём метод, то на коллайдере должен висеть собственный скрипт, чтобы это обработать. По этой причине у собираемых предметов в Spheramyd в корне префаба сразу находится Area, а вот у врагов там пустышка, но на внутренней Area свой посреднический скрипт, в котором проброшена связь с этой родительской пустышкой префаба. При инициализации родителя этот скрипт получает на него ссылку, а реагируя на столкновения перенаправляет их в вызов родительского метода.

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

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

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



Логика заклинаний
Из 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.


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