Unreal 5.6 C++ – Frontend UI with Common UI


This prototype is a focused deep-dive into building a complete frontend UI system in Unreal Engine 5.6 using Epic’s Common UI plugin, implemented primarily in C++ with Blueprint subclassing for asset configuration. The project covers a full title screen flow including a Press Any Key screen, Main Menu, and a tabbed Options screen with Video, Audio, Gameplay, and Controls categories — plus a reusable modal confirmation dialog system.

The main challenge was learning Common UI’s navigation model and input routing system, which operates very differently from standard UMG. In UMG, widgets handle input directly. In Common UI, each activatable widget becomes a focus scope when pushed to a stack, and input is routed through the topmost active widget automatically — including back-button and gamepad navigation.

The project was built following the UE5 C++ Advanced Frontend UI Programming course as a structural foundation. The C++ systems described below reflect my own implementation throughout the course.

📁 Source Code on GitHub


Widget Stack Architecture

The entire UI is organized around a single UWidget_PrimaryLayout root widget added to the viewport once per session. It owns four named UCommonActivatableWidgetContainerBase stacks identified by Native Gameplay Tags: FrontendUI.WidgetStack.Frontend, FrontendUI.WidgetStack.GameHud, FrontendUI.WidgetStack.GameMenu, and FrontendUI.WidgetStack.Modal.

Stacks are registered at runtime by Blueprint calling RegisterWidgetStack during initialization, and resolved by tag via FindWidgetStackByTag. No system holds direct object references to stacks — all navigation goes through the tag registry. Adding a new stack requires one new tag and one new RegisterWidgetStack call in Blueprint.

The UFrontendUISubsystem (a UGameInstanceSubsystem) is the central coordinator. It stores a reference to the primary layout widget and owns the widget push API. It is not created on Dedicated Servers, and supports derived classes — ShouldCreateSubsystem skips the base if a project-specific derived class exists.


Async Widget Push

All widget navigation is asynchronous. UFrontendUISubsystem::PushSoftWidgetToStackAsync async-loads the widget class via AssetManager::StreamableManager and pushes it via a two-phase callback: OnCreatedBeforePush (widget exists but is not yet visible — use this to configure it) and AfterPush (widget is active in the stack — use this to set focus).

Both phases are exposed as Blueprint output execution pins via two async UBlueprintAsyncActionBase nodes: UAsyncAction_PushSoftWidget for general widget navigation, and UAsyncAction_PushConfirmScreen for the confirmation dialog flow.

Widget classes are not hard-referenced in C++. They are stored as TSoftClassPtr in UFrontendUIDeveloperSettings (accessible under Project Settings → Frontend UI Settings), mapped by Gameplay Tag. UFrontendUIFunctionLibrary::GetWidgetClassByTag resolves the mapping at runtime. Adding a new screen requires one new tag and one new entry in Project Settings — no C++ recompilation needed.


Options Screen Architecture

The options screen is the most architecturally complex part of the project. It follows a strict Model-View separation:

  • UOptionsDataRegistry builds and owns the complete data object hierarchy. It acts as both factory and registry — creating all UListDataObject_* instances, binding their dynamic getters/setters, and configuring their edit conditions and dependencies.
  • UFrontendUICommonListView displays data objects as entry widgets. The correct widget class for each data object type is resolved at runtime via UDataAsset_DataListEntryMapping, which walks the data object’s class hierarchy for the best match.
  • UWidget_OptionsDetailsView shows contextual information (name, description, image, disabled reason) for the selected or hovered entry, driven by push from UWidget_OptionsScreen.

The data object hierarchy is a two-level tree: UListDataObject_Collection nodes represent tabs and sub-categories (Volume, Graphics, Advanced Graphics); leaf nodes are the individual settings.


Dynamic Getter/Setter Binding

Each settings data object is bound to a UFrontendUIGameUserSettings property via FOptionsDataInteractionHelper, which uses Unreal’s FCachedPropertyPath and PropertyPathHelpers to invoke getter and setter UFUNCTIONs dynamically at runtime. Bindings are declared using a macro that validates the function name at compile time:

cpp

#define MAKE_OPTIONS_DATA_CONTROL(FuncName) \
MakeShared<FOptionsDataInteractionHelper>( \
GET_FUNCTION_NAME_STRING_CHECKED(UFrontendUIGameUserSettings, FuncName))

GET_FUNCTION_NAME_STRING_CHECKED causes a compile error if the function name is invalid, preventing silent runtime failures from typos. This pattern allows the options registry to bind any GameUserSettings property to any data object type without writing per-property binding code.


Edit Conditions and Dependencies

Settings can declare edit conditions (FOptionsDataEditConditionDescriptor) that lock them based on runtime state. When a condition is not met, the entry is disabled, a rich-text reason is displayed, and optionally a forced value is applied. Examples:

  • V-Sync is locked when Window Mode ≠ Fullscreen, forced to false.
  • Screen Resolution is locked when Window Mode = Borderless Window, forced to the maximum allowed resolution.
  • Window Mode and Resolution are locked in editor builds — only adjustable in packaged builds.

Dependencies between settings are registered via AddEditDependencyData. When the dependency changes, the dependent setting re-evaluates its edit conditions. The relationship between OverallQuality and the individual quality settings (Shadow, Texture, Anti-Aliasing, etc.) is bidirectional: changing OverallQuality updates all individual settings; changing any individual setting notifies OverallQuality to re-read its scalability level, which may become “Custom” if settings no longer match a preset.


Confirmation Screen

The confirmation dialog is pushed onto the modal stack via UFrontendUISubsystem::PushConfirmScreenToModalStackAsync. Three layouts are supported: Ok, YesNo, and OKCancel. Configuration is carried by UConfirmScreenInfoObject — a data transfer object created before the widget is pushed — keeping the screen widget fully decoupled from the system that requested it.

Buttons are generated dynamically via UDynamicEntryBox. Focus defaults to the last button (the “negative” option — No/Cancel) to prevent accidental confirmation of destructive actions via gamepad or keyboard. The Reset action in the options screen uses this system, showing a “Are you sure?” dialog before resetting all settings in the current tab.


GameUserSettings and Hardware Benchmark

UFrontendUIGameUserSettings extends UGameUserSettings with game-specific settings: difficulty, volume levels, audio flags (background audio, HDR audio mode), and display gamma. All properties are UPROPERTY(Config), persisted automatically to GameUserSettings.ini. Display gamma is applied directly to GEngine->DisplayGamma for immediate effect rather than going through the Config pipeline.

On first launch, AFrontendUIPlayerController::OnPossess detects that no benchmark results exist (value = -1) and automatically runs RunHardwareBenchmark followed by ApplyHardwareBenchmarkResults, giving new players a reasonable initial video quality configuration for their hardware.


Tech Stack

  • Unreal Engine 5.6
  • Common UI plugin
  • Enhanced Input System plugin
  • C++ with Blueprint subclassing for asset configuration
  • Electronic Nodes and Blueprint Assist for Blueprint graph organization

Learning Source

Built following UE5 C++ Advanced Frontend UI Programming on Udemy.


Leave a comment

Create a website or blog at WordPress.com

Up ↑