1 July

Godot, 1000 мелочей

Open sourceProgrammingGame developmentPrototypingGodot
Недавно открыл для себя Godot engine, опенсурсный игровой движок. Делюсь некоторыми приёмами и заметками, в основном из области 3д, кода или общих моментов.


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

«Из коробки» в движке есть некоторая физика, в том числе джоинты и колёсный транспорт. Нет встроенного редактора terrain, но можно воспользоваться плагином, импортировать из специализированных программ или просто как меш из 3д пакета. В последнем случае ради производительности придётся самостоятельно резать ландшафт на фрагменты-чанки, и скорее всего делать им отдельные меши под коллизии. Что касается мешей под форму коллизии ландшафта — чтобы не было визуальных нестыковок, нужно триангулировать модель во время экспорта из 3д пакета. Например, один из простейших способов это сделать в Blender — экспортировать меш в формате collada (.dae), там по дефолту стоит галочка triangulate.
Иногда модель из Blender придётся переворачивать на 180 градусов внутри Godot, поэтому не удивляйтесь если вашего terrain'a не видно — скорее всего он повёрнут нормалями не в ту сторону.

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


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

Что касается языков, то если не рассматривать низкоуровневый способ, наиболее ходовые варианты — это скриптовые языки GDScript и C#, а также визуальный скриптинг для чего-то простого. GDScript лучше интегрирован в движок, имеет больше примеров, не требует запуска внешней среды, а в плане общей структуры тут будет происходить всё то же, что в варианте C# — объявление переменных, вызов функции инициализации, цикл процессов, цикл физических процессов и так далее. Так что выбор GDScript в качестве основного скриптового языка разработки имеет смысл. Пересаживаясь на местный C# получим разве что большую аккуратность и подробность записи, теряя в лаконичности, но усиливая разборчивость и контроль над ситуацией — фигурные скобочки вместо использования табуляции, отметки конца строки, более формальную типизацию.


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

Что касается глобальных переменных, то для их использования что в GDScript, что в C# потребуется добавить в параметрах проекта глобальный скрипт/скрипты в автозагрузку, к переменным которого можно будет обращаться глобально.
Также в Godot действует ограничение -на каждом объекте не может быть больше одного скрипта. Но этот момент можно обойти, например, повесив второй скрипт на дочерний объект.

Сигналы


Для обмена данными между узлами в Godot можно также воспользоваться иерархией, ведь объекты крепятся друг к другу, как ветки к дереву. Правда у этого подхода свои подводные камни, ведь в нашей игре иерархия может быть динамически изменяемой. К тому же обращение к иерархии в пределах одной сцены — это одно, а вот когда у тебя сцена внутри сцены внутри сцены, то с этим возникают некоторые сложности, хотя бы даже чисто в понимании происходящего.
Один из способов со всем этим управляться и не слишком привязываться именно к конкретной текущей иерархии — это сигналы. Какое-то количество общих сигналов уже предустановлено — можно заглянуть в панель сигналов объекта, чтобы прикрепить строку с обработкой получения одного из них в скрипт того же объекта или другого объекта со скриптом, в пределах сцены. Если же нужно сделать свой собственный сигнал, то делается это так:


Заводим сигнал

Излучаем его в том же скрипте при нажатии кнопки или других условиях

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


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

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


А вот и описание самого метода, который мы заводили выше. Получая сигнал он обрабатывает посланную с ним переменную.

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


Происходит событие и мы смотрим, есть ли у вошедшего в зону объекта нужный метод.


Метод есть, и он активируется, отправляя сигнал. Сам обладатель метода, кстати, не обязан иметь возможность вызывать эту свою функцию самостоятельно.

CSG-объекты



Один из полезных 3д инструментов в Godot — примитивы constructive solid geometry. Проще говоря это объекты, поддерживающие булевы операции — пересечение, исключение, объединение. Кроме набора примитивов есть универсальный CSG Mesh, в качестве формы для которого можно установить уже произвольный меш.



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


Пара сфер, из которых вырезаны дочерние сферы.

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

Следующее применение статических CSG — имитация разрушаемости и повреждений. для этого требуется расставить CSG примитивы в режиме исключения, как дыры, «огрызки» и вмятины, а затем временно скрыть, включая видимость в нужный момент. Ограничение тут в том, что «повреждать» мы можем только поверхность CSG объекта, к тому же «повреждение» должно быть изначально прикреплено к нему как дочернее либо крепиться через код, как потомок. Зато такой вариант по гибкости настройки уже значительно выигрывает по сравнению с заготовленным в 3д-пакете разрушаемым объектом.


В мост встроены 3 цилиндра в режиме исключения. Пока они скрыты мост целый.

Если их включить, то вырезается исключаемая ими область.

Далее у нас идут CSG движущиеся. Через код, либо записанную анимацию. В целом через этот способ можно реализовывать какие-то эффекты, но очень желательно чтобы подобные анимации не крутились на сцене в цикле. Несколько анимированных CSG могут заметно посадить производительность, к тому же в Godot оптимизирует не все вещи, и если вы не вырубите анимированные CSG самостоятельно, то они будут продолжать расходовать производительность вне видимости камеры.
Тем не менее эффекты анимированных CSG уже сложно заменить решениями 3д пакета, поэтому вы можете быть заинтересованы именно в их использовании (по крайней мере, если не собираетесь рисовать продвинутое 3д через код). Главное найти им правильное применение, лучше всего их использовать точечно, в определённых местах, включая анимацию по триггеру. Как эффект открытия прохода или прочий разовый спецэффект. Естественно, чем проще форма CSG объектов — тем лучше. А основную вычислительную нагрузку они оказывают именно в процессе соприкосновения в движении, притом нет особой разницы, какой именно объект двигать относительно другого.


На видео есть момент где машинка падает сквозь дыру в мосту, когда анимированная CSG-капсула проходит через CSG Mesh с моделькой моста.

Мультимеш



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


Кнопка для выбора целей мультимеша появляется в окне редактора справа вверху, когда он выбран. Если её нажать, то выпадет это окошко с установками для генерации.


Здесь мы имеем два объекта добавленные в сцену как узлы MeshInstance. Далее левый объект мы расклонируем по правому.


Получаем мультимеш. В данной ситуации в настройках было выставлено 10000 клонов. Кстати, если бы их было 3-4 тысячи, то визуально результат не сильно бы отличался.

«Запечёный» мультимеш можно переместить куда угодно, а кроме того у него есть параметр Visible Instance, изначально отображающий указанное при генерации число клонов. Меняя это значение можно управлять тем, сколько клонов показывается в данный момент. По дефолту там стоит -1, но это специальное значение для отображения максимума клонов. Если поставить 10, клонов будет 10. Если 100, то 100. Если максимумом было 50, то и при 100, и при 1000, и при -1 их так и останется 50.
Эту возможность мультимеша можно использовать для создания специальных эффектов, анимировав параметр Visible Instance через код или записанную анимацию. Для более тонкого контроля над плотностью клонов можно воспользоваться 3д пакетом, чтобы сделать отдельный меш с цельной сеткой именно в тех местах где нужна максимальная плотность клонов, а те места где их быть не должно, не покрывать полигонами, или сделать в них разрывы.


Устанавливаем Visible Instance в 500 и плотность заполнения резко снижается.

Разное



Управление в Годо можно настроить открыв пункт Проект в верхней панели редактора. Далее Настройки проекта, вкладка Список действий.


Можно посмотреть как называются дефолтные кнопки, чтобы знать по каким именам к ним обратиться через код. А также добавить свои собственные.

Например, для кнопки PgUP по дефолту установлено название «ui_page_up», и обработать её нажатие в коде можно написав следующую строчку в GDScript — if Input.is_action_pressed(«ui_page_up»):
Если требуется действие для однократного нажатия, то is_action_pressed нужно заменить на is_action_Just_pressed. Окончание нажатия — pressed меняется на released.

#

Если вам нужно анимировать мерцание материала то можно сделать это через самописный шейдер. Для этого в качестве материала объекта выбираем Новый ShaderMaterial




Далее в пустом поле выбираем VisualShader


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

Добавляем пару узлов, сначала Input — All — Time, затем Scalar — Common — ScalarFunc, выставив в её выпадающем списке, допустим, Sin. В принципе уже такая конструкция даст нечто вроде мерцания между чёрным и белым, но чтобы было получше, добавим ещё один узел Scalar — Common — ScalarFunc и выберем там Abs. Соединяем узлы между собой (обратите внимание, что нажимая на закрытый глаз у каждого узла можно открыть его и посмотреть как изменяется картинка на каждом этапе) и, подключаем к каналу Alpha, Emission или Albedo. Всё, наш объект теперь мерцает.



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

#

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

#

Чтобы пользоваться глобальными переменными, нужно создать скрипт и забросить его в автозагрузку: ПроектНастройки проектаАвтозагрузка. Галочка «синглтон» должна быть отмечена.


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

Чтобы обратиться к этим глобальным переменным из кода нужно сначала получить ссылку на синглтон, после чего пользоваться его переменными:


Находим наш SaveTheWorld, после чего проверяем, является ли состояние этого конкретного врага нулевым. Если да, то враг удаляется.

UPD. Можно обратиться к переменным синглтона сразу через его имя, то есть в примере выше нам не пришлось бы брать ссылку, и вместо main.enemy_arr[myID] обращались бы к SaveTheWorld.enemy_arr[myID]. Но вы должны быть уверены, что у этого узла в автозагрузке отмечена галочка в графе «синглтон».

#

Для записи и проигрывания анимаций на сцену добавляется узел AnimationPlayer. Он может находится в произвольном месте в иерархии. Для того, чтобы дать ему возможность записать анимацию нужно нажать на кнопку «Анимация», выше открывшегося таймлайна и создать новую анимацию.
После этого на каждом выбираемом узле в текущей сцене, справа в инспекторе появятся значки ключа. Если нажать на ключ, то аниматор предложит добавить новую дорожку анимации.



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

#

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



Для MeshInstance одними из наиболее затратных примитивов будут сфера и капсула, так как в них скорее всего будет много полигонов. Поэтому для 3д частиц лучше выбирать что вроде полигона, куба или призмы. Для коллизий, наоборот, форма сферы будет наиболее быстро считаемой, так как из параметров у неё один лишь радиус. Собственно, вышесказанное касается не только Годо, но и прочих игровых движков.
Также Godot может сделать форму коллизии для MeshInstance автоматически, для этого нужно нажать на кнопку «Массив», которая появляется вверху справа в редакторе, когда выбран MeshInstance. Далее выбираете из предложенных вариантов, но как правило, для того же terrain, это будет первый пункт — «Создать вогнутое статическое тело» (после чего к объекту прикрепится узел StaticBody с вложенной в него точной коллизией). Во всех прочих случаях, когда особо точные коллизии не требуются можно обходиться самостоятельно выставленными примитивами CollisionShape, либо собирать оптимальные меши для коллизий в 3д пакете, после чего добавлять их через MeshInstance и также создавать по их форме коллизию через кнопку «Массив».

#

Если в каком-то из числовых полей в инспекторе требуется, например, прибавить к параметру определённое число, умножить значение, и так далее, то можно так и выбивать: «315-180», «20+40», «64*5»… редактор сам посчитает и подставит итоговый результат операции в поле.

UPD:

Аудиофайлам по умолчанию (по крайней мере в 3.2.1) выставляется отсутствие проигрывания в цикле (loop) для .wav и зацикливание для .ogg. После закидывания файла в AudioStreamPlayer, например, там будет галочка для параметра loop, но выключить цикл для .ogg таким образом не получится. Чтобы убрать цикличность для таких файлов нужно выбрав файл переключиться на вкладку «Импорт» (рядом со «Сценой»), там убрать галочку с loop и нажать кнопку «Переимпортировать» ниже.

#

Когда есть некий объект, параметры которого нужно перенести на другой (положение в пространстве, вращение, масштаб), то, чтобы, не переносить каждое конкретно поле можно скопировать всё сразу, нажав на значок инструментов слева, в Инспекторе. Там в списке есть опция «Копировать параметры». Потом выбирается другой объект и через такую же иконку свойств выбирается опция «Вставить параметры».

#

Для удаления объекта через код обычно ему делают вызов queue_free(). Ещё быстрее можно удалить с помощью free(), но это менее безопасный вариант. Удаление слишком тяжёлых объектов вызывает заметную просадку fps, но лёгкие и оптимальные устраняются без просадок. В целом это некий индикатор — если объект удаляется слишком долго, то желательно оптимизировать его. Вызов remove_child(object) тоже похож на удаление, но по факту такой объект всё ещё хранится в памяти, чтобы его можно было снова к чему-то прикрепить, а если прикреплять заново не потребовалось, то эффект близок к вызову hide(), то есть к обычному сокрытию из видимости.
Tags:godotgamedevgdscriptopen sourcetutorialразработка игр3dигровой движокгодотзаметкиполезностипрограммированиекод
Hubs: Open source Programming Game development Prototyping Godot
+7
11k 52
Comments 24
Popular right now