Всем привет!

Во время работы над проектом наша инди-команда October Team столкнулась с серьезной проблемой: наша игра с трудом выдавала 30 FPS на среднем железе. Играть было неприятно, а о слабых системах и вовсе говорить не приходилось. Мы поняли, что без оптимизации этот проект просто не сможет выйти в свет.

Мы начали искать информацию об оптимизации игр на Unreal Engine. Наш поиск привёл нас к нескольким англоязычным статьям и видеороликам, которые описывали способы повышения производительности с использованием автоматизированных подходов. Однако, большинство материалов либо поверхностны и не рассматривают игру комплексно, либо ориентированы на крупные студии с большими ресурсами, и повторить их подход для инди-команды будет крайне трудно.

За несколько недель работы нам удалось практически удвоить FPS и добиться стабильной работы игры даже на слабых видеокартах (RX 580/GTX 1060) при одних и тех же настройках графики. Этот результат стал возможен благодаря комплексному подходу: автоматизации сбора данных, их анализу и устранению выявленных проблем.

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

Надеюсь, наши советы будут полезны начинающим разработчикам и инди-командам.

А зачем заморачиваться?

Разработка игр — сложный процесс, особенно для разработчиков-одиночек и небольших инди-команд. Часто во время работы над проектом можно столкнуться с “тормозами”, но определить, что именно их вызывает, бывает непросто. Интуитивный поиск проблемных мест на уровне игры, например, там, где частота кадров падает сильнее всего, позволяет устранить локальные проблемы, но не дает полной картины производительности игры.

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

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

В чем измеряем?

Частота кадров в секунду (FPS) — это основной показатель производительности, который мы обычно слышим. Однако, FPS — это упрощённая метрика, которая в совокупности складывается из работы нескольких последовательных процессов внутри Unreal Engine, отвечающих за создание каждого кадра:

  1. Game Thread — отвечает за игровую логику, например, обработку ввода, управление объектами и выполнение скриптов

  2. Render Thread — подготавливает команды для рендеринга и передает их на GPU

  3. GPU Draw — непосредственно отвечает за отрисовку кадра видеокартой

Также не стоит забывать о I/O (Input/Output) операциях, таких как загрузка текстур и моделей, влияющих на производительность игры.
Каждый из этих процессов занимает определённое время, которое измеряется в миллисекундах (ms). Именно это время в конечном итоге формирует общее время кадра (frame time). Поэтому для точного анализа производительности необходимо оценивать не FPS, а время выполнения каждого из процессов в миллисекундах. Данный подход позволит вам детально понять, какие аспекты требуют оптимизации, и сосредоточиться на устранении узких мест.

Вот так выглядит таблица соответствия ms и FPS:

FPS (кадры в секунду)

Время кадра (ms)

30

1000/30=33.3(3)

60

1000/60=16.6(6)

120

1000/120=8.3(3)

Далее, при анализе собранной статистики, сравнение времени и определение наиболее долгого процесса позволит точно определить, какой из них тормозит выполнение следующего кадра. Это даёт возможность целенаправленно и эффективно работать над оптимизацией.

Например, если после сбора данных выяснится, что Game Thread выполняется дольше, чем Render Thread и GPU — это означает, что вам надо искать проблему в логике работы ваших Actor, а не в текстурах или сложном освещении. Инструменты, которые предоставляет Unreal Engine, позволят точно определить, какой Actor или процесс внутри Game Thread вызывает долгую обработку кадра.

Как собирать данные?

При анализе производительности можно сразу вспомнить о плейтестах  — записи игрового процесса на различных устройствах. Однако, такой подход имеет серьезный недостаток: воспроизвести один и тот же путь персонажа и последовательность действий крайне сложно, даже если этим занимается один и тот же человек. Различия в движении камеры, управлении персонажем или взаимодействии с объектами приводят к различиям в нагрузке на систему, что усложняет сравнение результатов.

Для точного и воспроизводимого сбора статистики необходимо минимизировать влияние человеческого фактора. Оптимальный подход — автоматизировать процесс, чтобы статистика собиралась в одинаковых условиях, например:

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

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

  3. Скриптованные действия персонажа или NPC. Напишите Blueprint, который триггерит действия персонажа или NPC: ходить, взаимодействовать с объектами, совершать действия, нагружающие систему.
    Довольно времязатратный способ, но он, в отличие от предыдущих, позволит дополнительно автоматически проверять логику компонент.

Для нашего проекта мы использовали первый вариант — сбор статистики с нескольких заранее установленных камер. Такой подход наиболее прост в реализации и позволяет охватить весь уровень, если устанавливать камеры в местах, которые представляют наибольший интерес для анализа, например:

  • Точки с высокой детализацией сцены: зоны, где много геометрии и текстур. Для больших комнат или длинных коридоров мы устанавливали две камеры в противоположных углах или проходах

  • Участки с активными процессами и VFX: области, где работают сложные шейдеры, анимации или выполняются скрипты

  • Критические участки с возможными узкими местами: области с большим количеством NPC или интенсивной постобработкой

При установке камер мы рекомендуем вынести их на отдельный sub-level и заблокировать его, чтобы предотвратить случайные изменения или смещения камер.

Какие есть инструменты?

Unreal Engine предоставляет множество встроенных консольных команд и инструментов для анализа производительности, вот некоторые из них:

  • Набор команд stat <FPS/Unit/Game/GPU/…> позволяет вывести на экран статистику по выбранной характеристике. Например, stat unit покажет время на обработку кадра и каждого отдельного потока (GT, RT, GPU), это отличная команда для первоначального анализа

    Вывод команды stat unit

    Frame — общее время на отрисовку конкретного кадра
    Game — время, затраченное на работу Game Thread
    Draw — время, затраченное на работу Render Thread
    RHIT — Render Hardware Interface Thread, интерфейс для отображения графики на конкретной платформе
    GPU Time — время, затраченное GPU для отрисовки кадра
    DynRes — если поддерживается, покажет текущее разрешение
    Draws — количество обращений (вызовов) команд для отрисовки
    Prims — количество нарисованных треугольников

  • GPU Visualizer показывает, сколько времени и на что потратилось у GPU для обработки текущего кадра. Команда для вызова  -  ProfileGPU или сочетание клавиш Ctrl+Shift+, в редакторе

    Внешний вид GPU Visualizer

    Удобно то, что можно отсортировать по времени выполнения, чтобы сразу увидеть, какой процесс занял больше всего времени

  • Unreal Insights — инструмент для анализа производительности с подробным трейсингом по каждому потоку, который показывает иерархию вызовов и время выполнения, которое занимает каждый из них. Для его использования необходимо записать .utrace-файл, или можно подключиться к уже запущенной игре или ее симуляции внутри редактора. Команда Trace.Start <default/cpu/gpu/…> включит запись указанного потока в файл; default включает в себя потоки cpu, gpu, frame, bookmark и log. Для остановки записи используется команда Trace.Stop

  • RenderDoc — инструмент для детального анализа графики, который позволяет изучать, как GPU обрабатывает кадры, проверять текстуры, шейдеры и буферы. В отличие от Unreal Insights, RenderDoc фокусируется только на визуализации и помогает оптимизировать рендеринг

Для первоначального анализа производительности мы остановились на двух командах:  StartFPSChart и Trace.Start default
StartFPSChart соберет данные о железе, на котором запускалась игра, и предоставит время исполнения каждого потока (GT, RT, GPU) по кадрам, сохранив результаты в файл. Это позволит быстро оценить, какой из потоков занимает большее время. 
Trace.Start default соберет детальные данные по процессам внутри каждого из потоков и сохранит их в .utrace-файл для анализа в Unreal Insights. Это позволит отследить конкретные процессы, вызывающие задержки.

Пришло время собрать все вместе

Теперь, когда установлены камеры на уровне, и выбраны команды, которые мы будем использовать, приступим к автоматизации процесса сбора статистики по производительности. Создадим новый Blueprint типа Actor и разместим его на уровне.

Для статических камер алгоритм для Actor следующий:

  1. Получить список камер

  2. Отсортировать камеры, что бы порядок всегда был один и тот же

  3. По команде запуска в консоли: 

    1. Выполнить команды StartFPSChart и Trace.Start default

    2. Переключить View на камеру

    3. Выполнить Trace.Resume, подождать несколько секунд, выполнить Trace.Pause

    4. Если камер больше нет, 

      1. выполнить команды StopFPSChart и Trace.Stop

      2. иначе переключиться на следующую камеру и повторить с пункта 3.2

Для удобства получения списка камер установим для них общий тег. Для этого выберем камеру на уровне, и во вкладке Details поставим для нее тег. Нода Get All Actors with Tag не всегда возвращает объекты в одном и том же порядке, поэтому, чтобы последовательность камер была одинакова между разными запусками, полученный список необходимо отсортировать. Для сортировки списка мы использовали алгоритм пузырька, сравнивая имена камер. Чтобы запустить алгоритм из игровой консоли, создадим кастомный Event, выделим его ноду в редакторе, во вкладке Details включим галочку Call in Editor и привяжем к нему все последующие шаги алгоритма.

Так как I/O процессы тоже оказывают влияние на производительность, возможно, стоит добавить некоторую задержку при переключении камер перед включением Trace.Resume, чтобы дать время UE очистить память от объектов из предыдущей камеры.

Если вы решите использовать пролет камеры, то алгоритм несколько упростится, т.к. вам не придется делать сортировку и переключение между камерами.

Пример реализации подхода с расстановкой статичных камер:

Для алгоритма выше:

  • Reorder cameras соответствует пункту 1 и 2, получаем камеры по тегу, сохраняем и сортируем список

  • Start performance test является входной точкой для команды из консоли (пункт 3) и выполняет команды StartFPSChart и Trace.Start default (пункт 3.1)

  • Main performance loop

    • Change view to the performance camera соответствует пункту 3.2

    • Trace camera performance соответствует пункту 3.3

    • Check if last camera проверяет достигли ли мы последней камеры (пункт 3.4)

  • Reset camera, stop fps соответствует пункту 3.4.1

  • Move to the next camera соответствует пункту 3.4.2

Запускаем

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

Для получения реалистичных данных игру нужно собрать в test или development режиме. Чтобы выбрать режим сборки игры:

  1. Откройте Project Settings: в главном меню сверху, во вкладке Edit → Project Settings

  2. Введите вверху в строку поиска Build configuration, либо

    1. В левом меню, в разделе Project выберите Packaging

    2. Пролистайте вниз до вкладки Project, найдите параметр Build configuration

Также режим сборки игры можно выбрать непосредственно перед началом сборки:

  1. Откройте любой уровень в проекте и нажмите на Platforms в верхнем меню

  2. В списке Content/SDK/Device management наведите курсор на желаемую платформу

  3. В появившемся pop-up окне под пунктами Package Project и Cook Content выберите режим сборки

После сборки игры (Package project) запускаем игру, открываем консоль (`) и вводим команду ke * EventName (для примера выше команда будет ke * StartPerf). По завершении отработки логики нашего Performance Actor .utrace-файл для Unreal Insight будет сохранен в Saved/Profiling, а отчёты FPSChart сохранятся в папке Saved/Profiling/FPSChartStats.

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

Подводя итоги

В этой статье мы обсудили процесс настройки автоматического сбора статистики для оптимизации игры на Unreal Engine, которую можно сравнивать между итерациями разработки игры для оценки эффективности изменений и улучшений:

  1. Разобрали из чего состоит кадр в Unreal Engine — Game Thread, Render Thread, GPU и I/O

  2. Кратко изучили инструменты для анализа производительности предоставляемые Unreal Engine

  3. Рассмотрели подходы как можно получать повторяемые данные

    • статические камеры для фиксированных точек 

    • пролет камеры по заданной траектории

    • скриптинг действий для автоматизации работы персонажей и NPC

  4. С помощью нового Actor мы настроили переключение камер и сбор данных, автоматизировали процесс и исключили человеческий фактор

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

Используемые и дополнительные материалы


Группа нашей команды в VK - https://vk.ru/october10_team

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