How-To: Performance Stat Widgets

Goal: an FPS/ping readout that builds itself from a settings value - one block per stat the player switched on, flowing left to right and wrapping at the screen edge.

Where the numbers come from
UCrimsonPerformanceStatSubsystem (a game instance subsystem, created for everything except a dedicated server) samples all eighteen stats every frame and keeps a ring buffer of each. The widgets read it with no wiring - override FetchStatValue only for a stat that has to come from somewhere else.

1. One block per stat

Subclass Crimson Perf Stat Widget Base (UCrimsonPerfStatWidgetBase, a UCommonUserWidget) and give it up to three optional binds: Text_Label, Text_Value (both UTextBlock or any subclass, UCommonTextBlock included) and Graph_Value (UCrimsonPerfStatGraph).

That is the entire block. There is no event graph to write: the base ticks the visible halves for you, SetStatToDisplay(Stat) writes the localized name into Text_Label and restyles the graph, and GetFormattedValue() prints the value already scaled into its unit with its suffix and decimal places - 12.4ms, 73, 2%, 840 B. All of that per-stat presentation lives in one table in CrimsonPerfStatWidgetBase.cpp, because how a stat reads is a property of the stat rather than of the widget showing it.

StatToDisplay is an ECrimsonDisplayablePerformanceStat (PerformanceStats/CrimsonPerformanceStatTypes.h): ClientFPS, ServerFPS, FrameTime, FrameTime_GameThread, FrameTime_RenderThread, FrameTime_RHIThread, FrameTime_GPU, Ping, PacketLoss_Incoming, PacketLoss_Outgoing, PacketRate_Incoming, PacketRate_Outgoing, PacketSize_Incoming, PacketSize_Outgoing, Latency_Total, Latency_Game, Latency_Render, IdleTime. Override Fetch Stat Value (BlueprintNativeEvent, returns double) only when the number has to come from your own telemetry instead of UCrimsonPerformanceStatSubsystem.

Open the shipped W_CrimsonPerfStat (/CrimsonUI/UI/PerfStats/) as the reference: a 164 px SizeBox over a VerticalBox holding Text_Label, Text_Value and Graph_Value, and an entirely empty Event Graph. The names are the whole binding - a Text_Value spelled anything else is a block that silently never shows a number. Use it as-is, or copy the three names into your own layout.

Graph_Value is a Crimson Perf Stat Graph (UCrimsonPerfStatGraph) placed straight into the tree - it needs no Blueprint wrapper. GraphLineColor and GraphBackgroundColor style it in Class Defaults; its Y ceiling comes from the per-stat table, so a ping graph is not scaled like a frame time. UpdateGraphData() is BlueprintCallable if you want to drive it yourself, but the base already calls it every frame the graph is visible.

Verify
The number updates in PIE with no code at all. A stat that reads 0.0 forever is either one of the three latency stats (they need a latency-marker modular feature such as Reflex) or a net stat in a standalone session, which has no UNetConnection.

2. Let a container build the readout

Subclass Crimson Perf Stat Container Base (UCrimsonPerfStatContainerBase) and author exactly two things on it: a UWrapBox named Panel_Stats, and StatWidgetClass pointing at your block Blueprint.

Place no stat rows. On construct - and on every Refresh Stats call - the container walks the enum, asks GetStatDisplaySetting(Stat) about each stat, and creates, restyles or drops a block accordingly. A stat the player set to None does not exist as a widget at all, rather than existing and being invisible:

ModeEffect
HiddenNo block is created - and an existing one is dropped
TextOnlyText_Label + Text_Value; Graph_Value collapsed
GraphOnlyText_Label + Graph_Value; Text_Value collapsed
TextAndGraphAll three parts, in ONE block - never two widgets (default when you do not override)

Override Get Stat Display Setting (BlueprintNativeEvent) to read your project's own user settings - whatever object stores the player's "show performance stats" preference - and return an ECrimsonStatDisplayMode per stat: Hidden, TextOnly, GraphOnly, or TextAndGraph. Call Refresh Stats whenever the player changes them.

The blocks flow left to right and wrap onto a second line only when Panel_Stats runs out of width. For that to mean the screen, the slot holding the container has to be full width: if you place it through a UCrimsonUIExtensionPointWidget, set the point's EntrySizeRule to Fill - the default Automatic hands the entry only its desired width, and a wrap box's desired width is every block on one line.

Verify
With every stat set to None the readout draws nothing at all. Switch one to Text and Graph and a single block appears with its name, its number and its graph together; switch a second on and it appears beside the first, not under it.

See also

  • How-To: UI Extension Points - placing the stat container into a HUD slot, and the EntrySizeRule a wrap box needs there.
  • How-To: Tag-Driven HUD Visibility - hiding the whole readout with the rest of the HUD.