LyraTabListWidgetBase
设置界面 头上的三个TAB
FLyraTabDescriptor 是每个TAB的数据结构
实现 ULyraTabButtonInterface 接口的可以设置 Tab详情
ULyraTabListWidgetBase => UCommonTabListWidgetBase => UCommonUserWidget
ULyraTabListWidgetBase 这个还是 Abstract,看来是必须实现蓝图。
UCLASS(MinimalAPI, Blueprintable, BlueprintType, Abstract, meta = (DisableNativeTick))
class ULyraTabListWidgetBase : public UCommonTabListWidgetBase
{
private:
UPROPERTY(EditAnywhere, meta=(TitleProperty="TabId"))
TArray<FLyraTabDescriptor> PreregisteredTabInfoArray;
/**
* 注册但是未创建,创建就会删除
*/
UPROPERTY()
TMap<FName, FLyraTabDescriptor> PendingTabLabelInfoMap;
};
看起来会先根据 Tab 设置一个 map,然后在有一个异步创建的流程。
void ULyraTabListWidgetBase::NativeConstruct()
{
Super::NativeConstruct();
SetupTabs();
}
void ULyraTabListWidgetBase::NativeDestruct()
{
for (FLyraTabDescriptor& TabInfo : PreregisteredTabInfoArray)
{
if (TabInfo.CreatedTabContentWidget)
{
TabInfo.CreatedTabContentWidget->RemoveFromParent();
TabInfo.CreatedTabContentWidget = nullptr;
}
}
Super::NativeDestruct();
}
构造的时候 SetupTabs,销毁的时候 RemoveFromParent,看来 PreregisteredTabInfoArray 里面CreatedTabContentWidget是具体的创建出来的控件
PreregisteredTabInfoArray 没地方添加,看起来是配置的。
他的蓝图是 /Game/UI/Foundation/TabbedView/W_HorizontalTabList.W_HorizontalTabList 这个,【没找到,先继续看吧】
/**
* 当新标签页创建时委派广播。允许在创建后进行连接。
* 一个蓝图用,一个C++用
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTabContentCreated, FName, TabId, UCommonUserWidget*, TabWidget);
DECLARE_EVENT_TwoParams(UEqZeroTabListWidgetBase, FOnTabContentCreatedNative, FName /* TabId */, UCommonUserWidget* /* TabWidget */);
UPROPERTY(BlueprintAssignable, Category = "Tab List")
FOnTabContentCreated OnTabContentCreated;
FOnTabContentCreatedNative OnTabContentCreatedNative;
这里创建是代理
void UEqZeroTabListWidgetBase::SetupTabs()
{
for (FEqZeroTabDescriptor& TabInfo : PreregisteredTabInfoArray)
{
if (TabInfo.bHidden)
{
continue;
}
// 惰性地创建一次内容,并在切换器绑定发生变化时重用该内容
if (!TabInfo.CreatedTabContentWidget && TabInfo.TabContentType)
{
TabInfo.CreatedTabContentWidget = CreateWidget<UCommonUserWidget>(GetOwningPlayer(), TabInfo.TabContentType);
OnTabContentCreatedNative.Broadcast(TabInfo.TabId, Cast<UCommonUserWidget>(TabInfo.CreatedTabContentWidget));
OnTabContentCreated.Broadcast(TabInfo.TabId, Cast<UCommonUserWidget>(TabInfo.CreatedTabContentWidget));
}
if (UCommonAnimatedSwitcher* CurrentLinkedSwitcher = GetLinkedSwitcher())
{
// 将标签内容添加到新链接的切换器中。
if (!CurrentLinkedSwitcher->HasChild(TabInfo.CreatedTabContentWidget))
{
CurrentLinkedSwitcher->AddChild(TabInfo.CreatedTabContentWidget);
}
}
// 如果标签尚未注册(那个Map里面没有),则注册它
if (GetTabButtonBaseByID(TabInfo.TabId) == nullptr)
{
// 让父类创建并登记这个 Tab 按钮 + 建立按钮与内容实例的映射关系
RegisterTab(TabInfo.TabId, TabInfo.TabButtonType, TabInfo.CreatedTabContentWidget);
}
}
}
UCommonAnimatedSwitcher 有点眼熟呀 里面有个 TSharedPtr
找一下我们,Lyra的那个栈,确实也是这个。SCommonAnimatedSwitcher
可能把这个Tab做一些动画效果?
RegisterTab 吧Tab按钮注册到父类,目前还不知道细节,
我们先看看参数类型: FName, TSubclassOf
还有一些函数,我们断点走一下
打开设置界面:
W_LyraSettingScreen::RegisterTopLevelTab => RegisterDynamicTab
bool ULyraTabListWidgetBase::RegisterDynamicTab(const FLyraTabDescriptor& TabDescriptor)
{
// If it's hidden we just ignore it.
if (TabDescriptor.bHidden)
{
return true;
}
PendingTabLabelInfoMap.Add(TabDescriptor.TabId, TabDescriptor);
return RegisterTab(TabDescriptor.TabId, TabDescriptor.TabButtonType, TabDescriptor.CreatedTabContentWidget);
}
RegisterTab 里面 有个 HandleTabCreation
=> RegisterTab
=> HandleTabCreation
=> W_HorizontalTabList 蓝图里面了 调用 Event Handle Tab Creation
=> DebugCreateTab For Designer
=> HandleTabCreation_Implementation
目前可以知道,创建前吧东西加到这个MAP,
创建后删除,并且调用一下接口的 Execute_SetTabLabelInfo
void ULyraTabListWidgetBase::HandleTabCreation_Implementation(FName TabId, UCommonButtonBase* TabButton)
{
FLyraTabDescriptor* TabInfoPtr = nullptr;
FLyraTabDescriptor TabInfo;
if (GetPreregisteredTabInfo(TabId, TabInfo))
{
TabInfoPtr = &TabInfo;
}
else
{
TabInfoPtr = PendingTabLabelInfoMap.Find(TabId);
}
if (TabButton->GetClass()->ImplementsInterface(ULyraTabButtonInterface::StaticClass()))
{
if (ensureMsgf(TabInfoPtr, TEXT("A tab button was created with id %s but no label info was specified. RegisterDynamicTab should be used over RegisterTab to provide label info."), *TabId.ToString()))
{
ILyraTabButtonInterface::Execute_SetTabLabelInfo(TabButton, *TabInfoPtr);
}
}
PendingTabLabelInfoMap.Remove(TabId);
}
这里这个 TabButton 是 (Name="W_LyraButtonTab_C"_0)
创建Tab完成,点击一下。
到这里其实这个类已经差不多了。
接着蓝图找一下,设置界面上面的Tab是,/Game/UI/Foundation/TabbedView/W_HorizontalTabList.W_HorizontalTabList
他的事件【Event Handle Tab Creation => 父类 => Update Tab Styles】
Styles设置了一些颜值,比如Padding。CommonButton的 Style
【DebugCreateTab For Designer】设计模式,加三个TAB上来。
这俩没跑到
HandlePreLinkedSwitcherChanged
HandlePostLinkedSwitcherChanged
void UCommonTabListWidgetBase::SetLinkedSwitcher(UCommonAnimatedSwitcher* CommonSwitcher)
{
if (LinkedSwitcher.Get() != CommonSwitcher)
{
HandlePreLinkedSwitcherChanged();
LinkedSwitcher = CommonSwitcher;
HandlePostLinkedSwitcherChanged();
}
}
这里不知道什么情况会跑到TODO
===
UGameSettingPanel
UCLASS(MinimalAPI, Abstract)
class UGameSettingPanel : public UCommonUserWidget
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
TObjectPtr<UGameSettingListView> ListView_Settings;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional, BlueprintProtected = true, AllowPrivateAccess = true))
TObjectPtr<UGameSettingDetailView> Details_Settings;
};
ListView_Settings 是左边那个设置list,Details_Settings 是右边的详情面板
这个类是设置头顶几个切换按钮,点一下能切换具体内容的下面几个具体的面板
先看构造
UGameSettingPanel::UGameSettingPanel()
{
// 设置此小部件在被点击或通过导航到达时是否接受焦点
SetIsFocusable(true);
}
void UGameSettingPanel::NativeOnInitialized()
{
Super::NativeOnInitialized();
ListView_Settings->OnItemIsHoveredChanged().AddUObject(this, &ThisClass::HandleSettingItemHoveredChanged);
ListView_Settings->OnItemSelectionChanged().AddUObject(this, &ThisClass::HandleSettingItemSelectionChanged);
}
接下来是
UnregisterRegistryEvents();
RegisterRegistryEvents();
维护
UPROPERTY(Transient)
TObjectPtr<UGameSettingRegistry> Registry;
这是一个UObject,不过是 Settings Plugins 的内容了,细节过多,我们先当他是引擎的内容。
用用项目修改的代码开始。
总结Settings代码结构
有点乱
有点过度深入了,这里有个Lyra插件的Settings。过早的深入出不来了。
跳出来
之前其实有看过大致结构,在 Settings/ 目录下
UCLASS()
class ULyraGameSettingRegistry : public UGameSettingRegistry
{
UPROPERTY()
TObjectPtr<UGameSettingCollection> VideoSettings;
UPROPERTY()
TObjectPtr<UGameSettingCollection> AudioSettings;
UPROPERTY()
TObjectPtr<UGameSettingCollection> GameplaySettings;
UPROPERTY()
TObjectPtr<UGameSettingCollection> MouseAndKeyboardSettings;
UPROPERTY()
TObjectPtr<UGameSettingCollection> GamepadSettings;
};
这个就是头顶哪几个TAB,视频设置,音频设置,
然后 class ULyraSettingsLocal : public UGameUserSettings 是对于 ini 文件的管理配置。
class ULyraSettingsShared : public ULocalPlayerSaveGame 这个是基于本地文件存储的配置管理。
其他文件只是把Cpp的实现财开了几个文件夹。
===
ULyraSettingsListEntrySetting_KeyboardInput 这是那个设置中的一行小按钮
Screens/ 不认识,用到了在看
CustomSettings/ 里面看起来是设置的Value结构。可以认为设置的数据结构是挂载UI上的一颗树。树的每一个节点是一个SettingsGame。是一个key=value的结构。他的Value的不同子类来承担不同的设置职责。
接着才是UI里面的几个类
class ULyraSettingScreen : public UGameSettingScreen
另外一个GameSettingPanel 是 Plugin的内容了。
ULyraSettingsShared & LyraSettingsLocal
好多,等等吧。维护了配置的存储,一个基于 ini文件的 GameUserSettings,一个是基于 LocalPlayerSaveGame 的存储。
基本是一些bool,枚举来描述性能
因为这两个好像在性能面板里面有用,所以又跑来看性能面板了
性能面板
【性能面板 总界面】
两个性能面板,一个只有文本在顶上,一个带曲线。
看看Game Feature Action 是 这样的
/Game/UI/PerfStats/W_PerfStatContainer_GraphOnly
HUD.Slot.PerfStats.Graph
/Game/UI/PerfStats/W_PerfStatContainer_TextOnly.W_PerfStatContainer_TextOnly
HUD.Slot.PerfStats.Text
- ULyraPerfStatContainerBase 性能面板的C++
- ULyraPerfStatWidgetBase 性能面板里面每一个小父类的 C++类
- ULyraPerfStatGraph 每一个小分类有一个对应的Graph部分来画性能曲线图。(还有一个Slate类在这里)
- ULyraPerfStatWidgetBase 性能面板里面每一个小父类的 C++类
【ULyraPerfStatContainerBase】 这个提供了一个函数
ULyraSettingsLocal::Get()->OnPerfStatDisplayStateChanged().AddUObject(this, &ThisClass::UpdateVisibilityOfChildren);
这个时候更新,更新子节点的可见性,基于配置各个性能分类的配置。
=======
里面的每一个项是【ULyraPerfStatWidgetBase】不管是不是带曲线的
【ULyraPerfStatWidgetBase】里面的绘制曲线的控件是 【ULyraPerfStatGraph】
这个Graph的slate 是 SLyraLatencyGraph
===
结构差不多了先开始拼UI
先从性能面板,显示存文本那一行开始。
W_SingleTextStat
/Game/UI/PerfStats/W_SingleTextStat.W_SingleTextStat
这个控件显示出来大概是【FPS: 60:ms】显示在
在【Event Pre Construct】和 【Event Tick】主要是为了显示 【xxx: 0.0ms】 做的一些逻辑,还有字符串的format等
这里 Fetch Stat Value 的返回,显示在面板上 0.0ms 那个位置
他去一个性能的SubSystem 获取数值
double UEqZeroPerfStatWidgetBase::FetchStatValue()
{
if (UEqZeroPerformanceStatSubsystem* Subsystem = GetStatSubsystem())
{
return CachedStatSubsystem->GetCachedStat(StatToDisplay);
}
else
{
return 0.0;
}
}
除此之外,这个c++类还有的逻辑是,这个Slate画性能曲线的控件。
/**
* 一个可选的统计图表小部件,用于显示该统计数据随时间变化的值。
*/
UPROPERTY(BlueprintReadWrite, meta=(BindWidget, OptionalWidget=true))
TObjectPtr<UEqZeroPerfStatGraph> PerfStatGraph;
另外就是这个性能sub system的缓存
// 缓存 性能 subsystem 的指针
UPROPERTY(Transient)
TObjectPtr<UEqZeroPerformanceStatSubsystem> CachedStatSubsystem;
PerformanceStatSubsystem
关于上面的性能Sub system 看看。
他的核心逻辑其实是围绕 FLyraPerformanceStatCache 的
// Subsystem to allow access to performance stats for display purposes
UCLASS(BlueprintType)
class ULyraPerformanceStatSubsystem : public UGameInstanceSubsystem
{
protected:
TSharedPtr<FLyraPerformanceStatCache> Tracker;
};
主要是把这个 Tracker 注册消费者
void ULyraPerformanceStatSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Tracker = MakeShared<FLyraPerformanceStatCache>(this);
GEngine->AddPerformanceDataConsumer(Tracker);
}
void ULyraPerformanceStatSubsystem::Deinitialize()
{
GEngine->RemovePerformanceDataConsumer(Tracker);
Tracker.Reset();
}
// 向引擎注册一个性能数据消费者;它将在每帧中接收性能信息
ENGINE_API void AddPerformanceDataConsumer(TSharedPtr<IPerformanceDataConsumer> Consumer);
有哪些指标?
// Different kinds of stats that can be displayed on-screen
// 可以在屏幕上显示的各种统计数据
UENUM(BlueprintType)
enum class EEqZeroDisplayablePerformanceStat : uint8
{
// stat fps (in Hz) 客户端帧率(Hz)
ClientFPS,
// server tick rate (in Hz) 服务器 Tick 频率(Hz)
ServerFPS,
// ...
};
这个Tracker 或者说消费者,里面对于每一个新年都维护了一个数据 FSampledStatCache
// Observer which caches the stats for the previous frame
struct FLyraPerformanceStatCache : public IPerformanceDataConsumer
{
/**
* 缓存当前可用的各项性能统计数据的采样数据
*/
TMap<ELyraDisplayablePerformanceStat, FSampledStatCache> PerfStateCache;
}
FSampledStatCache 是干嘛的呢?
/**
* Stores a buffer of the given sample size and provides an interface to get data
* like the min, max, and average of that group.
* 定长采样缓存”,核心用途是把最近 N 次性能数据存下来,并快速提供统计结果(均值、最小值、最大值)。
*/
class FSampledStatCache
{
private:
const int32 SampleSize = 125;
int32 CurrentSampleIndex = 0;
TArray<double> Samples;
};
这样我们这个文件就清楚了,用一个 PerformanceStatSubsystem 来注册性能的 消费者。
消费者【PerformanceStatCache】需要实现三个函数
//~IPerformanceDataConsumer interface
virtual void StartCharting() override;
virtual void ProcessFrame(const FFrameData& FrameData) override;
virtual void StopCharting() override;
//~End of IPerformanceDataConsumer interface
排序Get接口,核心逻辑其实是 ProcessFrame
第一段 RecordStat 就是直接把数值存进 这一帧的数据结构了。
void FEqZeroPerformanceStatCache::ProcessFrame(const FFrameData& FrameData)
{
// 记录有关帧数据的统计信息
{
RecordStat(
EEqZeroDisplayablePerformanceStat::ClientFPS,
(FrameData.TrueDeltaSeconds != 0.0) ?
1.0 / FrameData.TrueDeltaSeconds :
0.0);
RecordStat(EEqZeroDisplayablePerformanceStat::IdleTime, FrameData.IdleSeconds);
RecordStat(EEqZeroDisplayablePerformanceStat::FrameTime, FrameData.TrueDeltaSeconds);
RecordStat(EEqZeroDisplayablePerformanceStat::FrameTime_GameThread, FrameData.GameThreadTimeSeconds);
RecordStat(EEqZeroDisplayablePerformanceStat::FrameTime_RenderThread, FrameData.RenderThreadTimeSeconds);
RecordStat(EEqZeroDisplayablePerformanceStat::FrameTime_RHIThread, FrameData.RHIThreadTimeSeconds);
RecordStat(EEqZeroDisplayablePerformanceStat::FrameTime_GPU, FrameData.GPUTimeSeconds);
}
// ...
}
然后好大一个if
void FEqZeroPerformanceStatCache::ProcessFrame(const FFrameData& FrameData)
{
// ...
if (UWorld* World = MySubsystem->GetGameInstance()->GetWorld())
{
// 看起来 ServerFPS是要我们自己从服务器同步下来的,每一帧服务器的GAverageFPS 同步下来
if (const AEqZeroGameState* GameState = World->GetGameState<AEqZeroGameState>())
{
RecordStat(EEqZeroDisplayablePerformanceStat::ServerFPS, GameState->GetServerFPS());
}
if (APlayerController* LocalPC = GEngine->GetFirstLocalPlayerController(World))
{
if (APlayerState* PS = LocalPC->GetPlayerState<APlayerState>())
{
// 记录 Ping 的统计信息
RecordStat(EEqZeroDisplayablePerformanceStat::Ping, PS->GetPingInMilliseconds());
}
// 记录一些与网络相关的统计信息
if (UNetConnection* NetConnection = LocalPC->GetNetConnection())
{
const UNetConnection::FNetConnectionPacketLoss& InLoss = NetConnection->GetInLossPercentage();
RecordStat(EEqZeroDisplayablePerformanceStat::PacketLoss_Incoming, InLoss.GetAvgLossPercentage());
const UNetConnection::FNetConnectionPacketLoss& OutLoss = NetConnection->GetOutLossPercentage();
RecordStat(EEqZeroDisplayablePerformanceStat::PacketLoss_Outgoing, OutLoss.GetAvgLossPercentage());
RecordStat(EEqZeroDisplayablePerformanceStat::PacketRate_Incoming, NetConnection->InPacketsPerSecond);
RecordStat(EEqZeroDisplayablePerformanceStat::PacketRate_Outgoing, NetConnection->OutPacketsPerSecond);
RecordStat(EEqZeroDisplayablePerformanceStat::PacketSize_Incoming, (NetConnection->InPacketsPerSecond != 0) ? (NetConnection->InBytesPerSecond / (float)NetConnection->InPacketsPerSecond) : 0.0f);
RecordStat(EEqZeroDisplayablePerformanceStat::PacketSize_Outgoing, (NetConnection->OutPacketsPerSecond != 0) ? (NetConnection->OutBytesPerSecond / (float)NetConnection->OutPacketsPerSecond) : 0.0f);
}
// 最后,如果启用了一些与输入延迟相关的统计数据,请记录下来。
// 这些信息也可以记录到CSV里面
TArray<ILatencyMarkerModule*> LatencyMarkerModules = IModularFeatures::Get().GetModularFeatureImplementations<ILatencyMarkerModule>(ILatencyMarkerModule::GetModularFeatureName());
for (ILatencyMarkerModule* LatencyMarkerModule : LatencyMarkerModules)
{
if (LatencyMarkerModule->GetEnabled())
{
const float TotalLatencyMs = LatencyMarkerModule->GetTotalLatencyInMs();
if (TotalLatencyMs > 0.0f)
{
// Record some stats about the latency of the game
RecordStat(EEqZeroDisplayablePerformanceStat::Latency_Total, TotalLatencyMs);
RecordStat(EEqZeroDisplayablePerformanceStat::Latency_Game, LatencyMarkerModule->GetGameLatencyInMs());
RecordStat(EEqZeroDisplayablePerformanceStat::Latency_Render, LatencyMarkerModule->GetRenderLatencyInMs());
// 这里可以用开关打开记录数据到CSV
// 过多的性能数据,如果需要的话
//LatencyMarkerModule->GetRenderLatencyInMs()));
//LatencyMarkerModule->GetDriverLatencyInMs()));
//LatencyMarkerModule->GetOSRenderQueueLatencyInMs()));
//LatencyMarkerModule->GetGPURenderLatencyInMs()));
break;
}
}
}
}
}
}
绕了一大圈,我们回到刚刚我们实现了一个【FPS:0.0ms】的数据控件
W_PerfStatContainer_TextOnly
/Game/UI/PerfStats/W_PerfStatContainer_TextOnly.W_PerfStatContainer_TextOnly
里面是顶上一排的性能显示,只有文本和数值。
每一项配置了 Stat to Display 决定了这个显示什么类型的数据。
/**
* UEqZeroPerfStatsContainerBase
*
* 显示性能数据的那个大面板,里面有很多个小控件,对应不同的性能数据(帧率、Ping、帧时间等等)。
*/
UCLASS(Abstract)
class UEqZeroPerfStatContainerBase : public UCommonUserWidget
{
public:
UFUNCTION(BlueprintCallable)
void UpdateVisibilityOfChildren();
protected:
// 我们要展示的是文本统计数据还是图表统计数据?
UPROPERTY(EditAnywhere, Category=Display)
EEqZeroStatDisplayMode StatDisplayModeFilter = EEqZeroStatDisplayMode::TextAndGraph;
};
这个是这个界面的父类,我们修改 StatDisplayModeFilter 的枚举,实现了一个只显示文本的性能界面。
通过绑定代理,在性能变化的时候调用 UpdateVisibilityOfChildren
void UEqZeroPerfStatContainerBase::UpdateVisibilityOfChildren()
{
UEqZeroSettingsLocal* UserSettings = UEqZeroSettingsLocal::Get();
const bool bShowTextWidgets = (StatDisplayModeFilter == EEqZeroStatDisplayMode::TextOnly) || (StatDisplayModeFilter == EEqZeroStatDisplayMode::TextAndGraph);
const bool bShowGraphWidgets = (StatDisplayModeFilter == EEqZeroStatDisplayMode::GraphOnly) || (StatDisplayModeFilter == EEqZeroStatDisplayMode::TextAndGraph);
check(WidgetTree);
// 遍历树上的所有控件
WidgetTree->ForEachWidget([&](UWidget* Widget)
{
if (UEqZeroPerfStatWidgetBase* TypedWidget = Cast<UEqZeroPerfStatWidgetBase>(Widget))
{
// 从 ini 读出每一个配置应该用 文本显示就够,还是需要画图显示。
const EEqZeroStatDisplayMode SettingMode = UserSettings->GetPerfStatDisplayState(TypedWidget->GetStatToDisplay());
bool bShowWidget = false;
switch (SettingMode)
{
case EEqZeroStatDisplayMode::Hidden:
bShowWidget = false;
break;
case EEqZeroStatDisplayMode::TextOnly:
bShowWidget = bShowTextWidgets;
break;
case EEqZeroStatDisplayMode::GraphOnly:
bShowWidget = bShowGraphWidgets;
break;
case EEqZeroStatDisplayMode::TextAndGraph:
bShowWidget = bShowTextWidgets || bShowGraphWidgets;
break;
}
// 最后根据情况显示对应的控件
TypedWidget->SetVisibility(bShowWidget ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed);
}
});
}
刚刚我们拼UI的时候在界面摆了很多子控件,每个控件都有一个枚举。如果ini配置了这个性能项目和界面的显示类型匹配(文本显示 or 图片显示)
就显示出来
配置一下看看把
/Game/UI/PerfStats/W_PerfStatContainer_TextOnly.W_PerfStatContainer_TextOnly
HUD.Slot.PerfStats.Text
炸,settings local 没有配置
UEqZeroSettingsLocal* UEqZeroSettingsLocal::Get()
{
return GEngine ? CastChecked<UEqZeroSettingsLocal>(GEngine->GetGameUserSettings()) : nullptr;
}
DefaultEngine.ini
[/Script/Engine.Engine]
GameUserSettingsClassName=/Script/EqZeroGame.EqZeroSettingsLocal
跑起来了但是没有数据。
我们从这里开始检查 class UEqZeroSettingsLocal : public UGameUserSettings
这里是ini文件的读取。
我们找的是这个字段
EEqZeroStatDisplayMode UEqZeroSettingsLocal::GetPerfStatDisplayState(EEqZeroDisplayablePerformanceStat Stat) const
{
if (const EEqZeroStatDisplayMode* pMode = DisplayStatList.Find(Stat))
{
return *pMode;
}
else
{
return EEqZeroStatDisplayMode::Hidden;
}
}
加载走 LoadSettings 接口。
保存 SaveSettings 没有重写。是用注解序列化的。
UPROPERTY(Config)
TMap<EEqZeroDisplayablePerformanceStat, EEqZeroStatDisplayMode> DisplayStatList;
底层的逻辑大概是这样的
显式调用 SaveSettings()(如 RunAutoBenchmark 后)
│
▼
UGameUserSettings::SaveSettings()
│
├─ Scalability::SaveState(GGameUserSettingsIni)
│ └─ 将画质等级(sg.ShadowQuality 等)写入 [ScalabilityGroups] Section
│
└─ SaveConfig(CPF_Config, *GGameUserSettingsIni)
└─ 将所有 UPROPERTY(Config) 字段序列化写入磁盘
包括 EqZeroSettingsLocal 中的:
DisplayStatList, bEnableLatencyFlashIndicators,
bEnableLatencyTrackingStats, DisplayGamma,
FrameRateLimit_*, MobileFrameRateLimit,
UserChosenDeviceProfileSuffix, bUseHeadphoneMode,
OverallVolume, MusicVolume, SafeZoneScale ... 等
这个保存的文件 GGameUserSettingsIni 是什么呢
惰性加载的,第一次拿的时候加载
const UGameUserSettings* UEngine::GetGameUserSettings() const
{
if (GameUserSettings == NULL)
{
UEngine* ConstThis = const_cast< UEngine* >( this ); // Hack because Header Generator doesn't yet support mutable keyword
ConstThis->CreateGameUserSettings();
}
return GameUserSettings;
}
void UEngine::CreateGameUserSettings()
{
UGameUserSettings::LoadConfigIni();
GameUserSettings = NewObject<UGameUserSettings>(GetTransientPackage(), GEngine->GameUserSettingsClass);
GameUserSettings->SetToDefaults();
GameUserSettings->LoadSettings();
}
配置文件是谁这里定
void UGameUserSettings::LoadConfigIni(bool bForceReload/*=false*/)
{
#if PLATFORM_WINDOWS
// ...
#endif
FConfigContext Context = FConfigContext::ReadIntoGConfig();
Context.bForceReload = bForceReload;
Context.Load(TEXT("GameUserSettings"), GGameUserSettingsIni);
}
- 先看命令行是否有
-GameUserSettingsINI=...覆盖; - 没覆盖则拼成 GeneratedConfigDir + Platform + "/GameUserSettings.ini"(标准化路径后返回)。
Saved\Config\WindowsEditor\GameUserSettings.ini
原来在这
[/Script/EqZeroGame.EqZeroSettingsLocal]
DisplayStatList=((ClientFPS, TextAndGraph))
这个是设置界面里面的,还没有设置界面,从Lyra偷一下配置
试一试乱填,数据读空了,界面不显示。OK的。
但是我配置了 ClientFPS。服务器的 FPS为什么显示了而且在跳。。。枚举配错了,改过来OK
===
下面我们把Graph加上来。
W_PerfStatContainer_GraphOnly
/Game/UI/PerfStats/W_PerfStatContainer_GraphOnly.W_PerfStatContainer_GraphOnly
这个界面
/EqZeroCore/UserInterface/W_EqZHUDLayout.W_EqZHUDLayout
加到Layout上面去
其中一个控件是这个
/Game/UI/PerfStats/W_SingleGraphStat.W_SingleGraphStat
这个控件大概长这样
----------------------
| ClientFPS |
| 曲线 0.0ms |
----------------------
这里的曲线是 Slate画的
这个控件构造的时候,设置了Stale的一些预设参数,在C++也写死了,一般不用改。
MaxYValue 是蓝图里面修改的,例如FPS就是 100。这样等会 当前/100 归一化 [0, 1] 再乘高度
void UEqZeroPerfStatWidgetBase::NativeConstruct()
{
Super::NativeConstruct();
// 在构造时缓存子系统,这也将确保图表是最新的
GetStatSubsystem();
if (PerfStatGraph)
{
PerfStatGraph->SetLineColor(GraphLineColor);
PerfStatGraph->SetMaxYValue(GraphMaxYValue);
PerfStatGraph->SetBackgroundColor(GraphBackgroundColor);
}
}
void SEqZeroLatencyGraph::DrawTotalLatency(const FGeometry& AllottedGeometry, FSlateWindowElementList& OutDrawElements, int32 LayerId) const
{
if (!GraphData)
{
return;
}
static TArray<FVector2D> Points;
Points.Reset(GraphData->GetSampleSize() + 1);
const FVector2D WidgetSize = AllottedGeometry.GetLocalSize(); // 120, 32
const float LineThickness = 1.0f;
const double XSlice = WidgetSize.X / static_cast<double>(GraphData->GetSampleSize()); // 控件宽度 / 样本数量 = 每个样本在X轴上占的距离
const double Border = 1.0;
int32 i = 0;
GraphData->ForEachCurrentSample([&](const double Stat)
{
// MaxYAxisOfGraph 是配置的最大值,归一化[0, 1] 再扩大到 WidgetSize.Y 的范围。
// 然后因为 Slate 的坐标系是左上角为原点,所以 Y 还要用 WidgetSize.Y 减去这个值。
double Y = WidgetSize.Y - FMath::Clamp((Stat * ScaleFactor), 0.0, MaxYAxisOfGraph) / MaxYAxisOfGraph * WidgetSize.Y;
// Y 要 Clamp [0, WidgetSize.Y] 的范围呢,在缩一个Border
Y = FMath::Clamp(Y, Border, WidgetSize.Y - Border);
Points.Emplace(XSlice * double(++i), Y); // 每一个点间隔 XSlice
});
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
Points,
ESlateDrawEffect::NoPixelSnapping,
LineColor,
false,
LineThickness);
}
嗯,这样性能设置这块基本OK了
总结
在Lyra中,设置界面可以开启各个性能的显示
LyraPerformanceStatTypes.h
- 描述了性能显示模式(不显示,仅显示文本,图片和文本)
- 有哪些性能项目(ClientFPS, ServerFPS...)
LyraPerformanceStatSubsystem.h
- 通过一个 UGameInstanceSubsystem 向引擎注册性能的消费者 AddPerformanceDataConsumer
- 这个 Consumer 需要实现 IPerformanceDataConsumer 接口 里面的 ProcessFrame 接口实现一下
- ProcessFrame 接口会把 FFrameData 给你,但是其他的你要自己去凑齐,比如去NetConnect
界面的绘制
ULyraPerfStatContainerBase 界面控件的容器,他遍历 Widtree 树,去拿性能的枚举,再去上面的SubSystem拿到数值。
显示到界面上。
===