Различные принципы, которые должен понимать разработчик, чтобы повысить производительность игры. Многое из перечисленного касается не только движка Godot.

Код

  • I

Первая вещь - следует кэшировать ссылки на объекты. То есть, в Godot, вместо постоянного использования в коде конструкций типа какого-нибудь $Camera.translation = $Hero.translation стоит заранее объявить ссылки на камеру и героя, и далее использовать уже их. То есть, например, пишем в шапке кода, до функции _ready():

onready var camera = $Camera

и

onready var hero = $Hero

а далее в коде оперируем уже этими ссылками, например, так:

camera.translation = hero.translation

В Godot 4 форма записи не сильно отличается - сначала заводим ссылку:

@onready var camera = $Camera

И далее пользуемся, например, меняя camera.environment или прочие параметры.

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

export var path_to_camera : NodePath

или

export (NodePath) var path_to_camera в Godot 3х

@export var path_to_camera : NodePath в Godot 4

Правда, так мы получаем только строку, обозначающую путь к объекту и далее в коде нужно будет завести отдельную переменную var camera = null , чтобы присвоить ей ссылку на объект уже внутри стартующей функции _ready():

camera = get_node(path_to_camera).

Однако, в Godot 4 можно сделать, чтобы в поле экспорта сразу можно было добавлять сцену/узел, минуя шаг предварительного получения пути (подобный способ - добавлять ссылки через поля экспорта, плотно практикуется в Unity, например):

@export var camera : Node

Почему не стоит обращаться к объектам через $ ($Camera, $Target, $StartMenu/start_button) в основном коде? Потому, что это некая упрощённая форма записи команды поиска по иерархии, при вызове перебирающая объекты сцены, чтобы найти нужный. То есть эта форма записи на деле вызывает функцию поиска по имени (get_node), которая занимает некоторое время и в зависимости от количества объектов в сцене и прочих факторов это время может быть значительным. Поначалу трата времени на обработку поиска через $ не так заметна, но когда сцена становится сложнее, объектов и скриптов становится много и вызов $ происходит в циклах - потери уже становятся значительными.

Практика писать примеры с частым использованием всевозможных $Name везде, в том числе внутри циклов, пришла из всяких простеньких обучающих видео и материалов по Godot, где кода всего несколько строк, оно просто работает и можно не думать о лучших практиках.

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

Что касается загрузки ресурсов в Godot - через load и preload можно ссылаться на путь к определённому файлу, который мы сможем инстанцировать и поместить эту копию внутрь текущей сцены. В иных движках, например в вёб-фреймворке PlayCanvas (да и в Unity), к ресурсам можно обращаться не через путь к ним, а по уникальному ID (UID) ресурса, что в чём-то упрощает жизнь (а в чём то и нет). В Godot полноценного UID не было, но в 4-ке оно уже заложено и его собираются ввести полноценно чуть позже.

  • II

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

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

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

func _process(delta):

  if $Level1/Enemy1.health < 1 and $Level1/Enemy2.health < 1 and ... and $Level1/EnemyN.health < 1:

    doGameOver()

мы, допустим, пишем внутри кода самих врагов проверку на Game Over (в данном случае обращение к отдельно заведённому синглтону Global_script), когда они получают урон и в том случае, если этот урон убивает их:

func getDamage(amount):
  health -= amount
  if health < 1 and is_exist:
    is_exist = false
    Global_script.tryGameOver()

Вместо Global_script тут мог бы быть, допустим, родительский узел для каждого Enemy, ссылку на который они получали бы через уровень иерархии или в коде самого родительского объекта. А сама функция tryGameOver() могла бы просто считать, сколько раз её вызвали и сравнивать с количеством врагов на уровне, заканчивая игру при выполнении условий. Как вариант - все вражеские сущности могли быть реализованы как единый класс или отправляли бы сигналы всей своей группе, об изменении их общего количества.

В то же время объявлять в цикле разные временные переменные для дальнейшего использования - это нормально. Например всякое такое:

var string = ""

или

var translation = hero.translation

или

var place = Vector3.ZERO

Делать постоянные проверки в цикле (if ... :) - это тоже само по себе нормально, просто желательно с ними не перебарщивать. Опять же, если какие-то проверки или части кода не обязательно должны срабатывать слишком часто, то может иметь смысл перенести их из цикла _process() в цикл _physics_process(), который "тикает" медленнее или вовсе написать таймер, чтобы делать какие-то сложные проверки с ещё меньшей частотой. Некоторые вещи из цикла _process() наоборот, вытаскивать нежелательно, например, какое-нибудь следование камеры за персонажем, иначе оно будет срабатывать уже не так плавно (хотя, если сам персонаж физический, то и следование камеры за ним скорее всего переедет в _physics_process(),чтобы опять же всё не дёргалось).

  • III

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

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

2D

  • I

Всё что может быть в атласах - должно быть в атласах. Если это будет ещё и квадрат со сторонами равными степеням двойки - совсем отлично (если нет то... скорее всего умный игровой движок сам как-то "склеит" все эти неправославные ошмётки изображений в power-of-two-fiiendly вариант где-то под капотом, но, конечно же, не слишком то эффективно). То есть не стоит лепить отдельную картинку на каждый отдельный 2д элемент - желательно собирать отдельные картинки в общий атлас. Собственно, движок может склеивать отдельные изображения, но лучше собирать картинки в атласы заранее. Ещё стоит разделять изображения с прозрачностью (какие-то элементы интерфейса) и непрозрачные (атлас с квадратными картинками персонажей, например, или тайлами) в разные атласы (но это уже задача со звёздочкой).

  • II

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

3D

Трёхмерная графика съедает основную долю производительности в играх, поэтому оптимизировать в первую очередь приходится именно её. Желательно смотреть на показатели профайлера - fps, draw calls - запуская игру в редакторе и тестируя её.

Темы lod'ов и "запечки" света казаться не буду - это область скорее для крупных вылизанных проектов, для разработки под консоли/мобилки, и пайплайнов по которым работают большие команды, чтобы выжать максимально красивую и правильную картинку, а не для большинства инди-разработчиков. Опять же, далеко не всем проектам это нужно исходя из выбранной перспективы, жанра, платформы. Например, когда игра с видом сверху, и высота камеры не меняется, то и lod'ить особо нечего. Или когда игра не для телефона, и без запечки света всё и так выглядит более менее, да и весит меньше.

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

  • I

Всё что может быть в атласах - должно быть в атласах, дубль два. Так как чем меньше материалов в сцене - тем лучше: меньше draw calls, выше fps. То есть, на одной текстуре желательно расположить развёртки нескольких моделей, а не одной. Правда это не всегда удобно, допустим, развёртка каждого отдельного игрового персонажа чаще всего занимает целую текстуру. Здесь опять же есть масса нюансов с тем, какой у конкретной игры сценарий использования материалов в каждый момент времени - если у вас всегда один действующий персонаж игрока в сцене, то почему бы ему не иметь свой отдельный материал, а если таких персонажей всегда сразу 10, то вот здесь уже можно задуматься о том, не совместить ли их материалы каким-то образом.

Отдельный способ покраски, почти бесплатный с точки зрения производительности - vertex painting, то есть древний способ покраски вертексов модели. "Бесплатный" он потому, что формат самой 3д-модели обычно уже содержит информацию о rgb цвете всех вершин и вертексной покраской вы всего-лишь меняете уже лежащие там дефолтные цифры с белого на цветной вариант. Развёртка и текстура для покраски по вертексам не требуется, но и красить можно только сами вертексы.

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

Если говорить про Godot, то в 3х через давний вариант формата .obj вертексный цвет не передавался, зато его переносил collada-формат .dae, например. В обновлённом формате .obj может переносить вертексную покраску из того же Blender, но Godot 3x новый .obj вроде не читает (по крайней мере если в нём включена передача вертексной покраски), а в Godot 4 уже перешли на gltf (хотя прошлые форматы в нём поддерживались и пока поддерживаются, но с определённой версии на них ругается редактор).

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

Слева монохромная текстура, справа вертексная покраска
Слева монохромная текстура, справа вертексная покраска
Соединение монохромной текстуры с вертексной покраской
Соединение монохромной текстуры с вертексной покраской

Также следует понимать, что ключевой момент - это количество материалов, а не самих текстур (хотя они тоже важны). Так как материал - понятие составное и может включать в себя несколько текстур, а в то же время из одной текстуры можно сделать кучу разных материалов, чуть другого цвета, с чуть иными настройками, и так далее. Если у вас 20 материалов в сцене из всего одной текстуры в качестве основы - это 20 РАЗНЫХ материалов.

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

В Godot 3 можно было отдельные меши закидывать в формате .obj, чтобы затем закидывать их в любые MeshInstance. В прочих форматах меш загружается внутри дополнительной обёртки, из которой его надо было ещё вытащить и просто так загрузить его в MeshInstance на разных сценах было бы нельзя (если не преобразовать в префаб и передавать префаб и тиражировать уже его), если не пересохранить тот отдельный меш в какой-нибудь формат .tres.

В Godot 4 .obj остались, но уже не рекомендуются с определённой версии, а при импорте .glb файла получается обёртка из которой даже единичную сетку нужно вынимать (если не планируется целиком превратить это в префаб, или это меш с костями, который разбирать обычно не нужно). Правда можно пересохранить такой файл через реимпорт в формат .res и загружать в любой MeshInstance уже его. Просто с .obj файлами процесс был более очевидным и простым для понимания, был наглядно виден принцип. Зато через .glb можно закинуть сразу много мешей, разобрав их потом отдельно на префабы (или в пару кликов выдрать из него все меши в виде .res отдельно) и не используя в сцене сам "донорский" .glb файл - только далеко не всем очевиден такой пайплайн работы.

  • II

Свет. Лучший свет - это когда его нет. Поэтому в играх крупных студий принято запекать лайтмапы, даже если динамичный свет и тени тоже в игре будут. Но так как у нас разговор не про это, то следует просто уменьшать количество источников света в сцене и отключать тени у всего чего можно. Допустим, использовать один Directional light или даже точечный Omni для всей сцены. Или Directional light может лишь подсвечивать сцену, не генерируя тени, в то время как один или несколько Omni как раз могут создавать тени, располагаясь, допустим, в разных частях уровня или когда один источник света следует конкретно за персонажем. В принципе можно раскидать по уровню какие-то точечные источники не отбрасывающие теней, чисто как подсветку - это садит производительность не так мощно, как свет, отбрасывающий тени. Ещё можно выключить отбрасывание тени у тех объектов, которым тень не требуется. Вот, например, у вас на уровне есть пол - зачем ему отбрасывать тень? Незачем, чаще всего. Значит для пола можно отключить отбрасывание теней. А ещё можно сделать сам материал unshaded, чтобы на него вобще не влиял свет и тени.

  • III

Камера. Стоит отрегулировать дальность отрисовки так, чтобы в видимость не попадало слишком большое пространство. В Godot 3 и 4 камера из коробки умеет в алгоритм frustrum culling, который отсекает из отрисовки то, чего камера не видит. Важно понимать, что камера нарисует ВСЁ, что попало в область её видимости. Даже если один объект заслонён другими, или его размеры ужасно малы - камера его видит.

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

Что обычно может быть таким большим объектом? Да тот же самый terrain, которого в Godot по умолчанию нет (а большинству игр оно и не нужно), но он подключается плагинами. Если же делать terrain самостоятельно, то как минимум следует разбить его на части. В иных движках terrain также побит на фрагменты-чанки хотя это может быть не очевидно, плюс ему добавлен алгоритм генерации на основе редактируемой карты высот, и могут быть написаны какие-то дополнительные оптимизации или поддержка всяких фич, вроде динамической "разрушаемости" и всякого подобного. В некоторых проектах можно применить окклюдеры, имеющиеся в движке - это некие области, через которые взгляд камеры не проникает.

Вы можете подумать - ну, про все эти базовые алгоритмы отсечения и камеру все разработчики давно знают. Ведь знают? Наверное. Но не во вселенной, в которой вышел Silent Hill 2 remake, например. Где на локациях с туманом, сильно блокирующим обзор, при его отключении оказывается, что камера видит огромное пространство впереди и никто не позаботится сократить предел её зоны видимости до вменяемого значения, соответствующего наблюдаемой пользователем небольшой области вблизи героя. А возможно, это потому, что всё ещё хуже - может быть ВСЯ локация это единый меш с кучей материалов, то есть мы постоянно отрисовываем на камеру всю локацию вобще (хотя для каких то игр и такое нормально, но это не то случай).

В каких-то движках для камеры написан полноценный продвинутый алгоритм occlusion culling, чтобы отсортировать объекты и убрать те, которых не видно за другими. Тем не менее он может, наоборот, замедлять процесс в играх определённого типа. В Godot 4 добавлена возможность автоматически "запекать" статичные окклюдеры для 3d-мешей, помимо расстановки окклюзий вручную.

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

  • IV

Геометрия. Чем меньше треугольников - тем лучше. Вся геометрия, которая может быть сшита в один объект - должна быть сшита в один объект. НО, не забываем про frustrum culling - не сшивай в один меш слишком уж много геометрии, иначе камера не сможет её эффективно отсекать. И, не забываем про то, что в один объект мы обычно можем сшить ТОЛЬКО объекты на которых подразумевается один и тот же материал (хотя с этим есть нюансы, но лучше исходить из парадигмы, что на одном объекте лишь один базовый материал + возможные проходы с дополнительными, которые не меняют сути). Собственно, в разных движках всякие алгоритмы динамического/статического батчинга сами сшивают объекты с одинаковым материалом и не далее определённого расстояния в цельный меш, но ничто не мешает делать модели таким образом изначально (ну, а кто-то делает мегатекстуру и его можно понять - профит капитальный).

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

В общем, возвращаясь к геометрии - хорошо когда на сцене мало ОТДЕЛЬНЫХ объектов и они не слишком большие. Условно - вот у нас в игре есть меши-столы и меши-стулья, которые всегда одинаково расставлены рядом с теми столами. Материал у них один и тот же. Для пользователя визуально нет разницы - сколько фактических мешей на сцене, количество треугольников ведь одинаковое, что объединяй их все в единую сетку, что располагай вот так - пообъектно. Но для отрисовки разница есть, поэтому если объединить столы с расположенными вокруг них стульями в цельные объекты - то отрисовка ускорится.

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

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

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

  • V

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

Тем не менее, автоматические методы имеют свои минусы и это скорее вариант решения задачи "дёшево и сердито". Разработчик может оптимизировать компиляцию самостоятельно, воспользовавшись как раз тем обстоятельством, что камера видит сквозь объекты. Просто делаем несколько плоскостей-квадратиков-примитивов и размещаем их в области видимости камеры, за основными объектами (пол/стены/персонаж/заставка и т.д.). На каждую такую плоскость назначаем материал, который встретится в игре дальше (кроме тех материалов, что уже сразу попали на камеру на нормальных объектах - как материал игрового персонажа, которого мы поместили сразу на стартовый экран). Таким образом, загружая стартовый экран игра скомпилирует шейдеры всех имеющихся на нём материалов и запустив уровень мы уже не столкнёмся с компиляцией шейдера какого-нибудь сундука, когда до него впервые доберёмся. Естественно, на компиляцию всё-равно уйдёт положенное время, но мы сдвигаем его на тот момент, когда игрок ждёт загрузки. Опять же - таким образом не обязательно компилировать все все шейдеры сразу. Запуская второй уровень мы покажем в его начале на камеру уже материалы второго уровня, оформив это как часть "загрузки".

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

Но вы, допустим, проектируете уровень из "белых" коробок, вобще не назначая материалы, или покрасили какие-то из них просто в обычный сплошной цвет, без текстур и не сохраняя как материал. Откуда же статтеры здесь? Оттуда, что материал у всех этих накиданных на уровень чистых мешей тоже есть по дефолту, а у покрашенных он точно стал уникальным.

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

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

  • VI

"Лишние" эффекты и постобработка. Иногда можно снизить производительность на ровном месте, добавив технологические, но ненужные какому-то конкретному проекту вещи. Например, избыточную карту HDRI освещения вместо более простого задника, или страшный блум (касается в основном Godot 3 с рендером gles2 - хотя там можно вытянуть блум во что-то приемлемое, но без него обычно лучше), или не такой нужный блур (эффект размытия), или ещё что-то подобное.

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

Физика

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

Различные коллайдеры, как 3д, так и 2д, тоже стоит оптимизировать. В случае коллайдеров играет роль сложность описывающей его математики - чем меньше параметров тем быстрее его считать. То есть столкновение с кругом, в случае 2д, или сферой, в случае 3д, будут рассчитаны быстрее прочих вариантов, так как они заданы только числом радиуса (помимо самих координат, естественно). Вот их и стоит использовать в качестве самого простого средства отслеживания коллизий с площадями/объемами.

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

Конкретно с коллайдерами в Godot ещё есть нюанс, что отлючение узла, в потомках которого есть коллайдер или самого узла коллайдера не прекращает работу его коллизии. Для выключения коллизии необходимо изменить специальное свойство коллайдера. К тому же если коллайдеры копипастить, то это будут связанные копии. То есть, если менять размеры одного из этих коллайдеров, то у прочих они тоже могут меняться. Если это нежелательно, то для копии нужно сделать операцию make unique, чтобы она стала независимой.

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


  1. cArmius
    03.02.2025 18:49

    Отличная статья, спасибо!

    Где можно почитать (и что конкретнее гуглить) про графические движки, чтобы понять почему спекание объектов в один ускоряет прорисовку?

    Это как-то совсем не очевидно, ведь общее число треугольников не меняется. Та же штука вероятно и со склеиванием текстур в одну.

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