The only sane way to optimize your game is by having good profiling metrics in game code. Unreal Engine comes packed with several good profiling tools and “stat commands” along with Unreal Insights is what I will be covering today. It allows us to measure pieces of our (C++) code in different ways. In this short article I explain how you can use these metrics to your advantage, they are slightly different for the Stat Commands as for Unreal Insights and we will cover both.

It’s a good practice to add metrics to certain areas of your code early. As features may perform fine initially, but may degrade as content changes. Having profiling stats in place enables you to quickly understand what’s going on.

Types of Trace Metrics

The first available metric type is a cycle scope, it tracks how much time is spent in a certain function. The second metric type is a counter, this can be useful to track event frequencies or instance counts rather than a measure of time.

You can find more macros in the following locations in the engine source:

  • Source/Runtime/Core/Public/ProfilingDebugging/CountersTrace.h (Counters for Unreal Insights)
  • Source/Runtime/Core/Public/ProfilingDebugging/CpuProfilerTrace.h (Cycle stats for Unreal Insights)
  • Engine/Source/Runtime/Core/Public/Stats/Stats.h (older “stats” system to display in viewport, still supported by Insights)

Counters

With counters we can easily track frequencies of occurances or other types of metrics such as instance counts of particular objects. You can use this information in a variety of ways, such as deciding on good pool sizes for certain Actors, testing the instance counts against other metrics such as cycle stats to understand how frame performance scales with X number of something.

If you just want to use the counters for Insights and now the viewport stats then available Macros are slighly simpler to use.

Counters For Unreal Insights

Adding new Counters for Unreal Insights is very simple, define the following counter at the top of your cpp file:

TRACE_DECLARE_INT_COUNTER(CoinPickupCount, TEXT("Coins in World"));

As you can imagine, you can replace INT with FLOAT (TRACE_DECLARE_FLOAT_COUNTER) if you need decimal precision. You can now use the defined Counter in game code with the following macros:

TRACE_COUNTER_SET(CoinPickupCount, CoinLocations.Num()); TRACE_COUNTER_ADD(CoinPickupCount, SomeNumber); TRACE_COUNTER_SUBTRACT(CoinPickupCount, SomeNumber);

Counters for Stat Commands

For the older Stat commands system is works slightly difference since it requires a Category under which to be grouped and displayed (eg. STATGROUP_Game). These stat groups are how stats are organized, you can type console command stat game to show everything listed in the STATGROUP_GAME, or stat anim for everything under STATGROUP_Anim. You can define your own stat group by changing the following Macro:

DECLARE_STATS_GROUP(TEXT("My Group Name"), STATGROUP_MyGroupName, STATCAT_Advanced);

As an example, I like to keep track of how many Actors get spawned during a session, so I added a counter to the ActorSpawned delegate available in UWorld.

At the top of the cpp file I declare the stat we wish to track. In the function that is triggered any time a new Actor is spawned we place the actual counter.

// Keep track of the amount of Actors spawned at runtime (at the top of my class file)
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Actors Spawned"), STAT_ACTORSPAWN, STATGROUP_Game);
// Increment stat by 1, keeping track of total actors spawned during the play session (Placed inside the event function)
INC_DWORD_STAT(STAT_ACTORSPAWN); //Increments the counter by one each call.

The above example is nice to track occurances, but often you want to measure execution cost instead. For that we use cycle counters.

Cycle Counters

In the next example I want to measure CPU time spent getting “Modules” on the player’s Ship.

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;
}

In the next section we’ll go in how these stats can be displayed on-screen using the above two examples.

Showing profiling metrics in-game

Toggling can be done per category and multiple can be on screen at once. To show a stat you open the console window (~ Tilde) and type “stat YourCategory”. In my case it’s “stat LODZERO” as defined by the code snippet of the next section that defines the Category as STATGROUP_LODZERO.

Tip: To hide all displayed stats you can simply type: “stat none”.

Adding new profiling metrics to your game

As you can see it only takes a few Macros to set up your own metrics. The one missing piece is how to define your own category as used in the above examples. Here is an example of declaring a Category:

DECLARE_STATS_GROUP(TEXT("LODZERO_Game"), STATGROUP_LODZERO, STATCAT_Advanced); 
// DisplayName, GroupName (ends up as: "LODZERO"), Third param is always Advanced.

Add this to your game header so it can be easily included across your project. (eg. MyProject.h or in my case I have a single header for things like this called FrameworkZeroPCH.h)

Finally, it’s important to note you can also measure just a small part of a function by using curly braces.

void MyFunction()
{
    // This part isn't counted
   
    {
         SCOPE_CYCLE_COUNTER(STAT_GetSingleModuleByClass);
         // .. Only measures the code inside the curly braces.
    }

    // This part isn't counted either, it stops at the bracket above.
}

Extending Trace Data for Unreal Insights

To add trace details for your own game code, you can easily do so using the SCOPED_NAMED_EVENT.

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

First parameter is a custom name as it shows up in Unreal Insights, the second is the color for display in the Insights UI.

The example below has two examples, one tracing the entire function while the second variation is placed within curly braces which limits the trace to within those lines of code. The _FSTRING variant lets us specify runtime names, but does add additional overhead so it should be used with consideration.

bool URogueActionComponent::StartActionByName(AActor* Instigator, FName ActionName)
{
  // Trace the entire function below
  SCOPED_NAMED_EVENT(StartActionName, FColor::Green);

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

    // Bookmark for Unreal Insights
    TRACE_BOOKMARK(TEXT("StartAction::%s"), *GetNameSafe(Action));
			
    {
      // Scoped within the curly braces. the _FSTRING variant adds additional tracing overhead due to grabbing the class name every time
      SCOPED_NAMED_EVENT_FSTRING(Action->GetClass()->GetName(), FColor::White);

      Action->StartAction(Instigator);
    }
    }
  }
}

Conclusion

Stat commands are incredibly useful if used pragmatically and provide a quick insight in your game’s performance. Make sure you add stats conservatively, as they are only valuable if you get actionable statistics to potentially optimize. They add a small performance hit themselves and any stat that is useless just adds to your code base and pollutes your stats view.

You might be interested in my other C++ Content or follow me on Twitter!

References