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.
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.
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:
| Mode | Effect |
|---|---|
Hidden | No block is created - and an existing one is dropped |
TextOnly | Text_Label + Text_Value; Graph_Value collapsed |
GraphOnly | Text_Label + Graph_Value; Text_Value collapsed |
TextAndGraph | All 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.
See also
- How-To: UI Extension Points - placing the stat container into a HUD slot, and the
EntrySizeRulea wrap box needs there. - How-To: Tag-Driven HUD Visibility - hiding the whole readout with the rest of the HUD.