UE4:C++直接获取STATS相关值的方式

前言

本文基于4.27版本

UE本身提供了很多STAT命令来帮助实时查看游戏运行时的一些数据:

【Stat命令表】

在游戏运行时通过控制台输入对应指令即可

不过直接使用命令,只会在屏幕上实时看到数据,如果使用Insights或者Frondend等工具,则会同时获取到大量的信息,不方便对数据的整理

比如,需要对游戏内所有的蒙太奇资源进行检查,来排查某些蒙太奇是否在某个性能指标上存在问题,希望可以快速的自动化测试,记录数据方便快速地进行筛选并反馈给美术,就需要自己稍微组织一下逻辑,并且直接借一下STATS的统计数据

STATS统计数据

这里使用Cascade Particle来举例

按照官方文档所示【统计数据系统概述】

比如在Engine/Source/Runtime/Engine/Public/ParticleHelper.h这个文件中定义到的很多STATS:

ParticleHelper

1
2
DECLARE_STATS_GROUP(TEXT("ParticlesOverview"), STATGROUP_ParticlesOverview, STATCAT_Advanced);

STATGROUP_ParticlesOverviewSTATGROUP_Particles是GroupId,直接在控制台上使用的时候是直接输入stat ParticlesOverview或者stat Particles即可

1
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Sprite Particles"),STAT_SpriteParticles,STATGROUP_Particles, );

其中STAT_SpriteParticles则是一个计数器,声明一个DWORD类型的计数器统计数据。由上面的声明可知,该计数器的名字为Sprite Particles,StatID为STAT_SpriteParticles,GroupId为STATGROUP_Particles

全局搜索这个StatID可以找到:
STAT_SpriteParticles

Engine/Source/Runtime/Engine/Private/Particles/ParticleEmitterInstances.cpp中有:

1
INC_DWORD_STAT_BY(STAT_SpriteParticles, ActiveParticles);

给该计数器增加了指定值,指定值为ActiveParticles

同样在这个文件的另一个位置:

1
DEC_DWORD_STAT_BY(STAT_SpriteParticles, ActiveParticles);

给该计数器减少了指定值,指定值为ActiveParticles

如果开启了STAT,这两个宏就会在对应的逻辑位置被执行,从而改变了计数器的值

通过C++直接获取STATS数据

Engine/Source/Developer/FunctionalTesting/Public/AutomationBlueprintFunctionLibrary.h提供了一些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
UFUNCTION(BlueprintCallable, Category = "Automation", meta = (HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"))
static void EnableStatGroup(UObject* WorldContextObject, FName GroupName);

UFUNCTION(BlueprintCallable, Category = "Automation", meta = (HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"))
static void DisableStatGroup(UObject* WorldContextObject, FName GroupName);

UFUNCTION(BlueprintCallable, Category = "Automation")
static float GetStatIncAverage(FName StatName);

UFUNCTION(BlueprintCallable, Category = "Automation")
static float GetStatIncMax(FName StatName);

UFUNCTION(BlueprintCallable, Category = "Automation")
static float GetStatExcAverage(FName StatName);

UFUNCTION(BlueprintCallable, Category = "Automation")
static float GetStatExcMax(FName StatName);

UFUNCTION(BlueprintCallable, Category = "Automation")
static float GetStatCallCount(FName StatName);

按照前文的例子,这里函数中的GroupName指的就是STATGROUP_ParticlesStatName指的就是STAT_SpriteParticles

但是看一下函数的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void UAutomationBlueprintFunctionLibrary::EnableStatGroup(UObject* WorldContextObject, FName GroupName)
{
#if STATS
if (FGameThreadStatsData* StatsData = FLatestGameThreadStatsData::Get().Latest)
{
const FString GroupNameString = FString(TEXT("STATGROUP_")) + GroupName.ToString();
const FName GroupNameFull = FName(*GroupNameString, EFindName::FNAME_Find);
if(StatsData->GroupNames.Contains(GroupNameFull))
{
return;
}
}

if (APlayerController* TargetPC = UGameplayStatics::GetPlayerController(WorldContextObject, 0))
{
TargetPC->ConsoleCommand( FString(TEXT("stat ")) + GroupName.ToString() + FString(TEXT(" -nodisplay")), /*bWriteToLog=*/false);
}
#endif
}

这里能看到其实就是执行了一下命令行,并且实际要传进来的GroupName应该是去掉了STATGROUP_

而几个获取数据的函数,最后实际上是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#if STATS
template <EComplexStatField::Type ValueType, bool bCallCount = false>
float HelperGetStat(FName StatName)
{
if (FGameThreadStatsData* StatsData = FLatestGameThreadStatsData::Get().Latest)
{
if (const FComplexStatMessage* StatMessage = StatsData->GetStatData(StatName))
{
if(bCallCount)
{
return StatMessage->GetValue_CallCount(ValueType);
}
else
{
return FPlatformTime::ToMilliseconds(StatMessage->GetValue_Duration(ValueType));
}
}
}

#if WITH_EDITOR
FText WarningOut = FText::Format(LOCTEXT("StatNotFound", "Could not find stat data for {0}, did you call ToggleStatGroup with enough time to capture data?"), FText::FromName(StatName));
FMessageLog("PIE").Warning(WarningOut);
UE_LOG(AutomationFunctionLibrary, Warning, TEXT("%s"), *WarningOut.ToString());
#endif

return 0.f;
}
#endif

这里要传递进来的StatName应该是STAT_SpriteParticles这样的完整名字

但是在这里发现一个问题,比如,在ParticleHelper.h中定义了非常多的计数器,其中有一部分是没有办法通过这种方式获取到的

经过调试有发现:
StatsData

这里的数量确实没有所有的该Group下的计数器,也就是说并不是所有被定义过的计数器都可通过该接口来获取。

其中额外发现对于类似于STAT_SpriteParticles这种COUNT类型的计数器,实机的位置是在StatsData->ActiveStatGroupsCountersAggregate

20230901175713

如果要获取这一类的计数器,可以通过这种方式来获取,下面的代码是获取STAT_SpriteParticles的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#if STATS
if (FGameThreadStatsData* StatsData = FLatestGameThreadStatsData::Get().Latest)
{
for (FActiveStatGroupInfo ActiveStatGroup : StatsData->ActiveStatGroups)
{
for (FComplexStatMessage CountersAggregate : ActiveStatGroup.CountersAggregate)
{
const double IncAveValueDouble = CountersAggregate.GetValue_double(EComplexStatField::IncAve);
const double IncMaxValueDouble = CountersAggregate.GetValue_double(EComplexStatField::IncMax);
FName Name = CountersAggregate.GetShortName();
if (Name == TEXT("STAT_SpriteParticles"))
{
PeakCascadeSpriteParticleCount = FMath::Max(PeakCascadeSpriteParticleCount, static_cast<int>(IncMaxValueDouble));
AverageCascadeSpriteParticleCount = static_cast<int>(IncAveValueDouble);
}
}
}
}
#endif

UE4:C++直接获取STATS相关值的方式
http://muchenhen.com/posts/25258/
作者
木尘痕
发布于
2023年9月1日
许可协议