Lyra 设置性能界面 开发记录 1

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

找一下我们,Lyra的那个栈,确实也是这个。SCommonAnimatedSwitcher

可能把这个Tab做一些动画效果?

RegisterTab 吧Tab按钮注册到父类,目前还不知道细节,

我们先看看参数类型: FName, TSubclassOf, TObjectPtr CreatedTabContentWidget

还有一些函数,我们断点走一下

打开设置界面:

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类在这里)

【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拿到数值。

显示到界面上。

===

上一篇