Единственный разумный подход к оптимизации игры — это всегда иметь под рукой хорошие метрики производительности. Unreal Engine поставляется сразу с несколькими полезными инструментами профилирования. «Stat commands» — один из таких инструментов. Они позволяют нам измерять ряд показателей для различных фрагментов нашего (C++) кода. В этой небольшой статье я объясню, каким образом вы можете извлечь из этого пользу.

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

Типы доступных метрик

Первый тип метрик — счетчики циклов (cycle counters), которые отслеживают, сколько времени в миллисекундах тратится на выполнение определенной функции. Второй тип метрик — это произвольные счетчики, которые могут быть полезны, например, для отслеживания частоты событий.

Полный список доступных макросов можно посмотреть в Engine\Source\Runtime\Core\Public\Stats\Stats.h. Вы найдете там несколько дополнительных полезных способов отслеживания вашего кода.

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

В верхней части.cpp‑файла (в моем случае LZGameInstance.cpp) я объявляю статистику, которую хочу отслеживать. Сам счетчик мы разместим в функции, которая будет запускаться каждый раз, когда порождается новый актор. Обратите внимание, что STATGROUP_LODZERO, с помощью которого мы определяем новую категорию статистики (я расскажу об этом в следующем разделе), определен в моем коде в другом месте.

// Отслеживаем количество порожденных во время выполнения акторов (в верхней файла части моего класса)
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Actors Spawned"), STAT_ACTORSPAWN, STATGROUP_LODZERO);

// Увеличиваем stat на 1, отслеживая общее количество акторов, порожденных во время игровой сессии (размещается внутри функции события)
INC_DWORD_STAT(STAT_ACTORSPAWN); //Увеличиваем счетчик на единицу при каждом вызове.

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

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

DECLARE_CYCLE_STAT(TEXT("GetModuleByClass (Single)"), 
STAT_GetSingleModuleByClass, STATGROUP_LODZERO);
AWSShipModule* AWSShip::GetModuleByClass(TSubclassOf<AWSShipModule> ModuleClass) const
{
	SCOPE_CYCLE_COUNTER(STAT_GetSingleModuleByClass);

	if (ModuleClass == nullptr)
	{
		return nullptr;
	}

	for (AWSShipModule* Module : ShipRootComponent->Modules)
	{
		if (Module && Module->IsA(ModuleClass))
		{
			return Module;
		}
	}

	return nullptr;
}

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

Отображение метрик профилирования в игре

Собранная статистика отображается по категориям, и на экране могут быть отображены сразу несколько категорий одновременно. Чтобы отобразить статистику, откройте окно консоли (~ «тильда») и введите «stat ИМЯ_ВАШЕЙ_КАТЕГОРИИ». В моем случае это «stat LODZERO», как определено в фрагменте кода из следующего раздела, который определяет категорию как STATGROUP_LODZERO.

Совет: Чтобы скрыть всю отображаемую статистику, можно просто ввести: «stat none».

Добавление новых метрик профилирования в вашу игру

Как видите, для создания собственных метрик требуется всего несколько макросов. Единственный недостающий в продемонстрированных выше примерах элемент — это определение собственной категории. Вот пример объявления категории:

DECLARE_STATS_GROUP(TEXT("LODZERO_Game"), STATGROUP_LODZERO, STATCAT_Advanced); 
// Отображаемое название, название группы (в итоге получается: "LODZERO"), третий параметр — всегда Advanced.

Добавьте это в заголовок вашей игры, чтобы его можно было легко включить в любом месте вашего проекта. (например, MyProject.h или, в моем случае, у меня есть отдельный заголовок для подобных вещей под названием FrameworkZeroPCH.h)

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

void MyFunction()
{
    // Эта часть не учитывается
   
    {
         SCOPE_CYCLE_COUNTER(STAT_GetSingleModuleByClass);
         // ... Оценивается только код внутри фигурных скобок.
    }

    // Эта часть тоже не учитывается, оценка останавливается на скобке выше. 
}

Дополнение данных трассировки для Unreal Insights

Вы можете легко добавить необходимые вам детали в трассировки для собственного игрового кода с помощью SCOPED_NAMED_EVENT.

SCOPED_NAMED_EVENT(StartActionName, FColor::Green);
SCOPED_NAMED_EVENT_FSTRING(GetClass()->GetName(), FColor::White);

Первый параметр — это пользовательское имя, как оно будет отображаться в Insights, второй — цвет для отображения в пользовательском интерфейсе Insights.

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

bool USActionComponent::StartActionByName(AActor* Instigator, FName ActionName)
{

  // Трассировка всей функции ниже
  SCOPED_NAMED_EVENT(StartActionName, FColor::Green);

  for (USAction* Action : Actions)
  {
    if (Action && Action->ActionName == ActionName)
    {


    // Закладка для Unreal Insights
    TRACE_BOOKMARK(TEXT("StartAction::%s"), *GetNameSafe(Action));
			
    {
      // Ограниченный фигурными скобками вариант _FSTRING добавляет дополнительные накладные расходы на трассировку, связанные с захватом имени класса
      SCOPED_NAMED_EVENT_FSTRING(Action->GetClass()->GetName(), FColor::White);

      Action->StartAction(Instigator);
    }
  }
}

Заключение

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

Ссылки


В заключение напоминаем, что сегодня, 19 декабря, пройдет вторая часть мастер-класса по созданию AI на C++ в Unreal Engine 5. Основные задачи: настройка Behavior Tree для обнаружения и принятия решений, создание боевого поведения, объединение NPC в группы, совместное выполнение целей. Записывайтесь по ссылке.

Запись первой части можно бесплатно посмотреть на странице курса "Unreal Engine Game Developer. Professional".

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