Godot и сферический диаблоид в вакууме
Итак, я собрал прототип arpg-проекта Сферамида с теми механиками, о которых писал ранее прошлой в статье Поиграть в браузере/скачать можно здесь:
Ниже некоторые подробности, по игре и приёмам работы с движком Godot.
Особенности игровой структуры и механик
Сферические подземелья
В то время как с разбиением цельной сферы на единообразные, удобные в текстурировании элементы, имеются некоторые проблемы, сбор сферического уровня из отдельных "комнаток" частично решает этот вопрос. То есть вместо того чтобы замостить всю сферу единообразными элементами на 100%, мы составляем сеть "комнат" и "переходов", оставляя часть пространства неиспользуемым. Остаётся лишь более менее аккуратно стыковать "комнатки" друг с другом. Кстати, даже цельную сферу в любом случае стоило резать на отдельные части из соображений оптимизации, а "комнатный" подход этому изначально лучше соответствует.
С точки зрения текстурирования "комнаты" есть цельные, пол и стены единой моделью с одной текстурой, а есть разделённые, где у пола отдельная тайловая трипланарно наложенная текстура.
Потолки сделаны отдельными моделями, и по задумке должны показываться только те их них, которые находятся перед игроком (исключая тот, что над ним). Пока что отрисовываются все, что попали в зону видимость камеры. Планируется сделать коллайдер, который будет включать нужные.
Отдельной .obj моделькой экспортируется коллизия со стенами, на ней развёртка не нужна, так как она не будет текстурироваться. Внутри Godot модель коллизии добавляется в MeshInstance, для которого вверху основного окна появляются инструменты создания коллайдера на основе полигональной сетки.
Персонаж
В отличие от первой моей "сферической" игры Relight , где управление было "танковое" - здесь персонаж двигается в 8 фиксированных направлениях, благодаря чему его можно лучше рассмотреть, вид вокруг не так быстро меняется и в целом подход более диаблоподобный.
Предполагается, что когда персонаж погибает, то игра продолжается за другого, ранее найденного, пока есть запас таких персонажей, как это было реализовано в прототипе Chaosborn На данный момент в игре один уровень и перерождений не происходит.
Концентрированное здоровье
Герой может найти по 3 вида бутылочек маны и здоровья. Под использование каждой отведена отдельная кнопка в интерфейсе, срабатывающая пока есть зелья такого типа.
У шкал здоровья и маны есть параметр концентрации, который падает со временем. Простое зелье не даёт концентрации. Среднее - меньше ресурса, но и некоторое количество концентрации. Сильное - ещё меньше ресурса, но больше концентрации.
При низкой концентрации шкалы зелье восстанавливает ресурсы с задержкой. При средней концентрации - сразу. А при высокой - шкала постоянно регенерирует, пока концентрация не понизится до среднего значения.
Органический инвентарь
Разные предметы блокируют разное количество пустых окружающих ячеек, попадая в инвентарь. Если же стараться укладывать предметы вплотную, то можно уменьшить количество заблокированных клеток, высвободив больше места. Зелья не имеют блокирующего влияния.
Камни заклинаний
Отдельный вид предметов, которые могут лежать в инвентаре или в специальных слотах, где комбинации камней в верхней строке определяют доступные герою заклинания. Первый камень и второй устанавливают заклинание на левую кнопку мыши, а второй камень с третьим дают заклинание на правую кнопку мыши. На данный момент комбинации дают одно из 4-х заклинаний.
Кнопка действия
Предметы подбираются с пола автоматически, но для взаимодействия с некоторыми объектами требуется нажать кнопку E - сейчас это пустые капсулы-саркофаги. Также можно открывать двери, а также закрывать их. Для этого требуется зажать кнопку действия, тогда после некоторой задержки будет вызван метод открытия у последней обнаруженной поблизости двери.
Сами двери пока сделаны так - есть цельная модель каркаса и полностью закрытых дверей, есть модель с каркасом, где двери открыты. В момент открытия во время анимации также показываются дополнительные отдельные модельки створок, а одна цельная финальная модель меняется на другую. Таким образом, пока двери статичны - это всегда одна модель в кадре. С другой стороны, оптимальнее может быть оставлять каркас отдельной неизменной моделью, а для створок иметь цельную модель, когда они сведены. Естественно, можно было просто ограничиться всего 3-мя моделями - каркасом и отдельными створками, так как дверей в кадре всё равно не будет настолько много, чтобы лишние сетки стали какой-то заметной проблемой.
Утилитарная магия
При нажатии на пробел персонаж создаёт слабый магический выстрел, наносящий мало урона и медленно летящий. Он не тратит ману и всегда доступен, подходит для того, чтобы разбивать кувшины.
Враги
На данный момент имеется 3 вида противников - слизни, призраки и призраки-маги. Враги на уровне изначально пребывают в дезактивированном состоянии, но вокруг персонажа существует специальная большая зона, которая "размораживает" врагов при касании, или "замораживает" их обратно, если они вышли из её области действия.
Взаимодействие выстрелов персонажа с врагами устроено следующим образом - коллайдер выстрела настроен на режим мониторинга по маске, обозначающей противника. У монстриков коллайдер настроен только на обнаружение прочими, сам не следит за столкновениями, а коллизиях включен слой, обозначающий монстра. При столкновении выстрел вызывает в скрипте противника метод нанесения повреждений, а сам уничтожается.
Слизни просто путешествуют в том направлении, куда смотрят. Сталкиваясь с препятствиями меняют направление, а коснувшись игрока наносят ему повреждения.
Призраки мониторят окружающее пространство вращающимися рейкастами и, если обнаружат игрока - двигаются к нему. Если персонаж попадает в зону атаки, то призрак атакует. Призраки маги ведут себя похожим образом, но вместо ближних атак создают выстрелы и приближаются медленнее.
На самом деле призраки устроены таким образом, что в режиме обнаружения персонажа вращается сам их центр, расположенный в центре сферы уровня. Это нужно, чтобы враг разворачивался точно в сторону персонажа в момент попадания рейкаста. Для того, чтобы призраки не вертелись визуально вокруг своей оси, я сделал, чтобы сама моделька призрака компенсировала вращение центра, вращаясь в противоположную сторону.
Скрипты, алгоритмы, логика
Способы связи с объектами
Несмотря на наличие в Godot сигналов, чаще всего используются сигналы встроенные, вроде добавления функции от события нажатой кнопки. Использовать самописные сигналы обычно не требуются, если только не идёт речь о том, что постоянно через код в сцене будет возникать что-то динамическое, требующее прикрепления к себе сигнала. Но даже в этом случае можно вместо сигнала просто передавать создаваемому объекту ссылку на родителя или другой управляющий скрипт, к которому тот может обращаться, вызывая какие-то методы.
Что касается получения получения ссылок на фиксированные объекты сцены, для дальнейшего манипулирования ими, то здесь имеется два основных ходовых варианта - прописать путь к узлу в инициализации скрипта или прописать там же поля экспорта, в которые можно будет скинуть ссылки на эти узлы в окне редактора.
Те объекты, которые сами имеют на себе скрипты, могут при первом появлении в сцене отправлять ссылку на себя в глобальный синглтон. Естественно, если там заведены соответствующие поля, изначально обозначенные как null. Например, это может сделать сам главный скрипт, чтобы прочие могли обращаться к его методам. Также удобно использовать такой тип создания ссылок для множественных единообразных элементов имеющих скрипт, например, для кучи однотипных кнопок. То есть для каждой кнопки через экспорт в окно редактора выводится её порядковый номер, а при первом появлении на сцене скрипт кнопки отправляет ссылку на кнопку в глобальный синглтон, в массив на позицию с её номером. Теперь можно иметь доступ к кнопкам, например, из главного скрипта - допустим, перерисовать их текст, при нажатии на смену языка в настройках.
Узлы-обёртки
В большинстве случаев удобнее крепить отдельные специфические элементы (модель, спрайт, кнопка, текстовое поле) к простому стандартному узлу, который будет выступать в качестве контейнера. Это очень пригодится, например, при создании анимаций средствами движка - вместо того, чтобы непосредственно анимировать какую-то модель (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.
Спасибо за внимание.