Фантастические приёмы оптимизации — и как они называются
От экономных отражений до бюджетных звуков: рассказываем, на какие хитрости идут разработчики, чтобы игра получилась не только красивой, но и шустро работала.
Игра должна работать. И работать хорошо. За фасадом творчества, мозговых штурмов и необычных геймплейных концепций скрываются десятки и сотни мелочей — технологий, приёмов и хитростей, помогающих «выжать» из движка пару дополнительных кадров. А то и вовсе реализовать гениальные и захватывающие, но временами сомнительные технически задумки геймдизайнеров.
Сегодня поговорим о технологиях и приёмах, зачастую незаметных, но крайне важных. Они позволяют сделать игровой процесс комфортным, историю — увлекательной и атмосферной, а счётчик FPS — радующим глаз. Ведь даже простая внешне локация может стать настоящим вызовом для программистов.
Уже не удивляют, но без них никуда
С момента выхода первых игровых движков разработчики всегда старались сделать игры лучше: масштабнее, проработаннее, атмосфернее. Они работали над графикой, над качеством звука и картинки, учились учитывать каждую мелочь, проектируя новую локацию. И их «главным злодеем» (если не брать в расчёт бюджет) всегда было железо платформы, будь это консоль или ПК.
Дело в том, что каждое устройство (как тогда, так и сейчас) имело свои характеристики, которые неизбежно приходилось учитывать: объём оперативной памяти, количество ядер и частоту процессора, производительность видеокарты. Но игры-то делать хотелось! И чем круче выглядела и звучала игра, тем больше у неё было шансов хорошо продаться. Это сейчас никого не удивишь фотореалистичной графикой, а в начале нулевых хорошая картинка очень даже «роляла».
Поэтому разработчикам приходилось искать баланс между красотой картинки и цифрой на счётчике кадров, а то и вовсе искать пути, как обойти ограничения выбранной платформы или даже самого движка. Впрочем, часть технологий была изобретена до них.
Суть технологии проста: чем дальше объект, тем меньше в нём полигонов. Для этого заранее готовятся несколько моделей объекта: от проработанной до самой примитивной, а при появлении на сцене выбирается LOD в зависимости от расстояния до игрока. Если игрок подходит, то модель становится проработаннее, если удаляется — упрощается.
В результате мы получаем детализированную картинку и возможность хорошенько рассмотреть интерьер, пушку или тело противника «в упор». А разработчики добиваются плавной картинки, ведь модели вдалеке потребляют в разы меньше ресурсов. К тому же из них можно сделать красивый задний план, грамотно расположив низкополигональные растения или здания, или создать более достоверную (и экономную!) иллюзию масштабности игрового мира.
Правда, если порядок загрузки нарушен (или модель недостаточно упрощена), может случиться катастрофа. Например, в Cities Skylines 2 чудовищно проседал FPS по простой причине: у каждого жителя города подгружались… зубы, имеющие огромное количество полигонов.
Но LODы хорошо работают в играх с открытым миром. Если же пространства не так много, а экономить ресурсы платформы нужно, на помощь приходят коридоры. И лифты.
Лифты в Mass Effect отлично иллюстрируют ещё одну техническую хитрость — Level Streaming, или потоковую подгрузку уровней. Хотя игрокам они, скорее всего, запомнились не этим, а навязчивой музыкой и долгим ожиданием. Целиком загрузить достаточно проработанный уровень — задача крайне трудная, зачастую невозможная. Не хватит ни оперативной памяти, ни видеопамяти. Поэтому разработчики, если это позволяет дизайн игры, идут на хитрость.
Они дробят уровень на сегменты, оставляя на стыках специальные «бутылочные горлышки», существенно замедляющие персонажа: узкую шахту, ущелье, дверь или лифт. И пока игрок ползёт (или ждёт дезинфекции, как при выходе с корабля в Mass Effect), уровень незаметно подгружается, а мы получаем иллюзию бесшовности.
Подобный приём встречается и в Alien Isolation, и в The Callisto Protocol, и даже в Helldivers 2, где он реализован на пять с плюсом — вместо скучного лифта (прости, Шепард) мы наблюдаем эпичное падение капсулы с Адским Десантником на поверхность планеты.
Кстати, обычные загрузки в играх — ложь. Вернее, их полоски, которые зачастую программируются отдельно и создаются так, чтобы всегда был «движ» и игрок видел, что процесс идёт.
Настоящий процесс загрузки неравномерен, и замирание на десяток-другой секунд изрядно напрягало бы, поэтому разработчики пошли на хитрость. Никакой оптимизации тут нет, разве что психологическая — игрок, увлечённый движущейся полоской, не успевает заскучать.
С загрузками разобрались, но стоит упомянуть об ещё одном визуальном аспекте игр, над которым разработчики успели вдоволь «поиздеваться» — отражения.
Как их только не делали! Сначала просто зеркально повторяли целые локации, заменяя место отражения стеклом или невидимой стеной. Затем создавали дублёра игрока, повторявшего его действия в «зазеркалье». Именно так, например, поступили в Duke Nukem 3D. Но такую комнату приходилось создавать для каждого зеркала, подгружая дополнительное пространство. А это существенно влияло на производительность и ограничивало геометрию уровня — приходилось следить, чтобы отзеркаленные локации не накладывались на настоящие.
Поэтому разработчики искали другое решение. Его им предоставила NVIDIA с выходом карты GeForce 256 в 1999 году, анонсировав кубические карты отражений (CubeMap).
В плане реализации (для разработчиков) выглядело всё крайне просто. Брали локацию, «фотографировали» с шести сторон, а после наносили на текстуру, используемую для зеркал и других отражений в играх. С какой стороны подошёл, такая сторона куба и показывается. Просто? Просто.
Минусы очевидны: ни тебе правильного преломления света и перспективы, ни отражения объектов в реальном времени (никаких персонажей в зеркалах!), зато дёшево в плане производительности и крайне просто в реализации. Потому технология иногда применяется и в наше время. Правда, её усовершенствовали до Parallax Corrected Cube Maps, научившись растягивать и подстраивать изображение под камеру игрока.
Когда производительность платформ выросла, разработчики решили не изобретать велосипед и повторили трюк с дублированием комнат. Правда, с отличием: они не создавали в отражении вторую локацию, а поставили камеру, которая рендерила картинку с новой перспективы. А уже та, в свою очередь, отображалась в зеркале. Так придумали Planar Reflection — технологию, применение которой можно увидеть, например, в Max Payne 2 или Alien: Isolation.
Отражение есть? Есть! Вот только для его создания приходится рендерить вдвое больше игровых объектов: как с точки зрения игрока, так и с точки зрения камеры-зеркала. И если «зеркало» стоит в небольшой комнатке, это ещё терпимо. Но вот продублировать целую улицу, да ещё и оживлённую — уже задача непосильная для старых систем. Впрочем, эта технология получила второе дыхание в последние годы, прочно «прописавшись» в инструментарии Unreal Engine и Unity.
Но на момент появления технологии нагрузка всё ещё была достаточно большой, поэтому разработчики вновь начали искать компромисс. Им стали Screen Space Reflection, или же Экранные отражения. Этот шейдер ещё в далёком 2011-ом Crytek добавила в Crysis 2 и технологию можно назвать «примитивной трассировкой лучей».
Шейдер использовал информацию, доступную на экране, анализируя пиксели и создавая на основе них отражения. Вот только главное здесь — «на экране». Если какой-то объект хотя бы частично остаётся за кадром, то и его отражение в воде или в витрине не будет прорисовываться до конца. Поэтому в играх с SSR часто можно заметить такую картину:
Но метод позволяет получать динамические отражения в реальном времени с неплохой точностью и качеством, да и «пропадающие» отражения не бросаются в глаза в динамичных сценах. К тому же для отражения SSR использует только те ресурсы, которые видит сам игрок, что избавляет движок от необходимости повторно рендерить сцену, так что технология получила достаточную популярность.
Да и никто не запрещал комбинировать методы, что и провернули Insomniac Games. Их Человек-паук, летая по городу, спокойно разглядывает «кубмапы», но стоит ему приземлиться на улицу с плотным трафиком или в сюжетную локацию, как включается SSR, начиная показывать отражения людей и автомобилей. В боевых же сценах методы и вовсе объединяют — Cube Map начинают «показывать» статику (стены, потолок и мебель), а SSR — динамику (противников и героя).
Баланс и красота. Думаю, уже по отражениям с LODами понятно, насколько сильно разработчикам приходится изголяться в попытках выгрызть у движка пару кадров. Вот только помимо картинки есть ещё и звук, физика, анимация и логика! Как учесть всё это и не сойти с ума от очередного кранча?
Как понять, куда уходят FPS? Профайлер и ограничения «вызовов»
На помощь приходят настоящие герои оптимизации, без которых разработчики бы уже точно забросили своё дело — профайлеры. Именно эти инструменты помогают точно узнать, на что уходит время при отрисовке кадра, показывая буквально по долям миллисекунд, сколько времени занимает обработка разных объектов. Например, профайлер Unity позволяет узнать даже о скорости обработки конкретных команд в коде игры.
Чтобы игра выдавала 60 FPS, каждый кадр (в том числе и логика) должен отрисовываться не дольше, чем за 16,66 миллисекунд. Часть вызовов выполняется с помощью процессора, в то время как с остальным должна справиться видеокарта.
Вот только бывают ситуации, когда либо процессор не справляется с выданными задачами, заставляя «простаивать» каждый кадр видеокарту, либо, наоборот — процессор быстро «разбирается» с кодом, пока видеокарта заветные миллисекунды раздумывает над очередным высоко полигональным объектом. Именно знание о том, где конкретно закралось «бутылочное горлышко», позволяет разработчикам добиться нужной отметки FPS.
Для этого существует множество профайлеров, они есть у каждого гиганта индустрии (NVIDIA NSight, AMD GPU PerfStudio, Intel VTune, Wwise’s Profiler), а также в любом мало-мальски «серьёзном» движке.
Например, профайлер Unity позволяет не только отследить время вызова и отрисовки компонентов, но и узнать, на что тратится оперативная память при отрисовке сцены с помощью пакета Memory Profiler.
А также просмотреть каждый отдельный этап рендеринга с помощью Frame Debugger. Последнее позволяет воочию увидеть проблемы с отрисовкой отдельных элементов.
Найдя проблему, разработчики приступают к решению: переписывают код, оптимизируют библиотеки звуков и текстур, используют другие алгоритмы и технологии, а то и вовсе удаляют объект или целую механику из игры.
Впрочем, иногда делают и хитрее, «ограничивая» частоту части вызовов. Например, зачем игре каждую секунду спрашивать позицию противника, который находится где-то за стеной или в километре от игрока? В таком случае система «упрощает» сбор данных, уточняя нужную позицию лишь раз в 5-10 кадров.
Многопоточность и надо ли её искать
Когда наталкиваешься на стенания разработчиков о том, что им не хватает мощности процессора, так и хочется воскликнуть: «Да у вас же столько ядер! Где многопоточность?». Собственно, на Reddit именно так и поступают, время от времени задаваясь вопросом, почему большинство игр работают только на одном потоке. Но всё не так просто, как кажется.
Многопоточность и системы задач действительно поддерживаются современными движками, как и в Unreal Engine, так и в Unity существуют системы, которые позволяют распределить логику по потокам или даже кластерам.
Более того, сами движки по умолчанию стараются «раскидать» обработку данных на несколько потоков. Обычно выделяется главный (gameplay), а также несколько фоновых, отвечающих за рендеринг, подгрузку аудио, загрузку игровых ресурсов, расчёт физики и ИИ соответственно.
Такая система (или, скорее, подход к архитектуре проекта) называется DOTS (Data-Oriented Technology Stack). Он состоит из трёх компонентов.
Первый — это Entity Component System (ECS). Система, позволяющая работать не с отдельными игровыми объектами (GameObject), а с паттернами «сущность-данные-системы», где сущности — это, по сути, лишь болванчики с ID, который подчиняются отдельным системам. И уже в них хранится вся логика.
При правильной настройке подобное решение позволяет создавать физические симуляции, в которых участвуют 13 000 – 25 000 сущностей, а то и больше, при этом сохраняя счётчик FPS на отметке в 60.
Кроме того, в DOTS используется C# Job System — система, позволяющая в реальном времени распределять задачи (алогритмы и команды) по разным потокам для их правильного параллельного выполнения, и Burst Compiler, который собирает все скрипты в высокопроизводительный нативный код, который примерно в 10 раз быстрее по сравнению с обычным C#.
DOTS иногда буквально спасает студии, помогая реализовать задуманное. Например, в Diplomacy is Not an Option подход DOTS применяется практически везде в игре, позволив реализовать битвы с тысячами юнитов.
DOTS подходит для масштабных проектов, в которых участвуют тысячи сущностей, подчинённых одной логике. Система хорошо показывает себя в симуляции физики, процедурной генерации и в случаях, когда нужно одному алгоритму «подчинить» сразу тысячи объектов, чтобы те вели себя как одно целое. Более того, при правильной архитектуре кода «тяжёлые» игры с воксельной генерацией графики могут запускаться даже на старых мобильных устройствах с низкой производительностью.
А если в основе игры лежит процедурная генерация или рендеринг мира на лету, этот подход может даже спасти релиз. Так случилось с разработчиками SNØ: Ultimate Freeriding, которые смогли за три недели с помощью распараллеливания оптимизировать свой проект под Steam Deck, удвоив частоту кадров.
Казалось бы, идеал. Вот только есть одно «но»: синхронизация. Многопоточность есть почти во всех играх уже лет 20-25, просто обычно она заключается в одном потоке, ответственном за всю логику, и побочных «ветвях», отвечающих за рендер, подгрузку аудио и текстур и другие «дополнительные» процессы.
Попытки «распараллелить» логику тоже были, вот только обычно всё упирается в ограниченное количество потоков консолей, а также в сложность синхронизации: один «задумавшийся» поток может застопорить работу всей системы, а то и вовсе привести к вылету из-за неправильных данных.
Приходится ждать от него результатов или балансировать порядок/приоритет выполнения, рискуя получить десинхронизацию. При этом для подобного требуется буквально перерабатывать всю архитектуру кода, практически не получая ощутимого прироста FPS, если речь идёт о «главной» логике игры.
Поэтому разработчикам иногда приходится идти другим путём и освобождать ресурсы на необходимые вычисления в других частях своих проектов. Например, оптимизируя звуки.
Искусство звучать легко (и производительно!)
Некоторые считают, что оптимизация — это финальный процесс. То есть мы создаём игру, запускаем, а после смотрим, где и что можно подправить. Но при работе над крупными проектами студии сначала формируют «бюджет» ресурсов. Разработчики буквально делят игру на части, а после, исходя из характеристик планируемых платформ, прикидывают, сколько операций в тик и оперативной памяти они могут выделить на тот или иной аспект игры.
Так, разработчики Scars Above, используя при работе с аудио ПО Wwise, столкнулись с проблемой исчерпания памяти на PlayStation 4 и Xbox One. Поэтому, чтобы не перегружать процессор сложной системой пространственного звука, они решили условиться: на все звуки — не больше 250 Мб ОЗУ, а аудио поток должен загружаться максимум на 100% на 8 поколении консолей, но в среднем не более чем наполовину.
Правда, для этого им пришлось постараться. Дело в том, что пространственный звук — это не просто «врубить музыку или реплику в нужный момент». Система отслеживает источники звука в пространстве, учитывает окружение и его влияние на звучание, следит за тем, чтобы нужные звуки легко различались, а ведь их количество может исчисляться десятками.
Если в сцене слишком много звуков, не все из них успевают загрузиться и обработаться вовремя. В итоге мы сталкиваемся с тишиной вместо реплик, шагов или выкриков, звуковыми артефактами, задержками воспроизведения или даже с отсутствием нужных эффектов.
Если звук не был строго привязан ко времени, например, это была фоновая музыка или реплика диалога — разработчики просили систему «выбрасывать» его из памяти и не загружать заранее, освобождая ресурсы для более важных задач. А чтобы при старте разговора с NPC не возникало проблем, они всё же загружали небольшой фрагмент из его начала, выигрывая тем самым время.
Ещё одной хитростью стало «дробление» фоновых звуков. Вместо того, чтобы загружать один большой эмбиент, разработчики разбивали его на короткие сегменты, складывали в контейнер и запускали в режиме непрерывного цикла со случайным воспроизведением. Процессор слегка напрягался, но зато в памяти освобождалась лишняя пара мегабайт.
Похожим образом они разобрались и с «озвучиванием походки» главного героя, правда, вместо самих файлов изменяли тон, громкость и задержку при воспроизведении аудио-файла. Так, один и тот же звук шага всегда звучал уникально и отличался от предыдущего. Это довольно известный приём — питч-шифтинг. Правда, обычно изменяют только тон.
Так разработчики снизили загрузку процессора и потребление ОЗУ. Но им, как и всегда, пришлось столкнуться с дилеммой: как сделать атмосферный и насыщенный звук, уникальный для каждой зоны, но при этом не перегрузить проект? На помощь пришли виртуальные голоса, звуковые банки и уже знакомый Level Streaming. И в сумме они позволяют практически творить чудеса.
Каждый воспроизводимый звук в игре — это «голос». И для их воспроизведения требуются различные вычисления: декодирование аудиофайла, применение эффектов и фильтров, вычисление объёма пространства, в котором тот звучит. Если игрок «слышит» звук, он является «физическим», но если его источник находится слишком далеко, Wwise позволяет настроить этот голос как «виртуальный».
Виртуальные голоса пропускают все вычисления, кроме громкости, что сильно экономит ресурсы процессора. Поэтому разработчики решили ограничить количество голосов следующим образом:
И в игре, если громкость физического голоса становится тише -40 Дб, он попросту выключается и либо удаляется из игры (если его больше не будут использовать), либо становится виртуальным.
Но если на сцене могут находиться максимум 40 физических голосов, зачем нужны 500 виртуальных? А вот здесь уже вступают в дело звуковые банки и Audio Level Streaming. Первые являются сгруппированными наборами звуков. Например, это звуки интерфейса («Основные»), звуки, издаваемые персонажем («Игрок») или музыка. Эти банки, кстати, загружены всегда.
Но зато остальные наборы звуков загружаются только по необходимости. Например, при появлении соответствующих врагов, при переходе в другую локацию, старте диалогов или для усиления кинематографических эффектов. Для этого уровень делится на секции, которым даже не нужно делать лифты (победа!), ведь звуки невидимы. Когда игрок переходит из одной области, одни звуковые банки выгружаются из памяти, а другие появляются.
В результате игрок, перемещаясь по карте, создаёт звуковой облик игры, а разработчики укладываются в заветные 250 Мб ОЗУ и радуются отлично работающему звуку! Кстати, советую ознакомиться с полной статьей (на английском) всем заинтересованным в игровом звуке, разработчики выкатили тонну полезной информации на эту тему.
И ведь как только разработчики не изгаляются над движком игры и своими проектами, пытаясь затолкать в брыкающиеся системы побольше эффектов, красивостей и «глубин проработки». Каждый подобный приём — компромисс между производительностью и проработкой, а также порой прекрасная демонстрация смекалки программистов и геймдизайнеров.
И в этой статье я рассказал далеко не обо всём — настолько «далеко», что позже выпущу второй текст о других приёмах. Как программисты игровой логики «подглядели» концепцию LODов, с помощью чего в The Last Of Us 2 создавали столь плавные анимации и как разработчики обманывают игроков, незаметно превращая «мощные и крутые» 3D-модели в «скучные и пережатые» 2D-спрайты на лету? Подписывайтесь на блог, чтобы не пропустить продолжение.