DEV Community

GameDevToolLab
GameDevToolLab

Posted on

Choosing UI Tools in Unreal Engine 5: UMG, CommonUI, Slate, Blueprint, and C++

Designing a UI in Unreal Engine 5 is not a binary choice between Widget Blueprints and C++. The more useful starting point is to separate four responsibilities:

  • State ownership: Characters, Actor Components, PlayerStates, or Subsystems hold gameplay state.
  • Presentation: A thin adapter forwards simple values; a Presenter or Viewmodel handles more complex display transformations.
  • Widget ownership: A PlayerController, HUD, or ULocalPlayerSubsystem-based UI manager creates and retains screens.
  • Input routing: The PlayerController, CommonUI Action Router, or UI manager coordinates input, focus, and mouse capture.

This guide uses Unreal Engine 5.8 as its technical baseline. It covers UMG, CommonUI, Slate, Widget Components, and UMG Viewmodel, then connects those choices to both Blueprint-first and C++-first implementations.

The default approach is straightforward: start with UMG, keep layout and visual iteration in Widget Blueprints, and place state, ownership, and subscription contracts in Blueprint or C++ according to the project's needs. The important boundary is the responsibility—not the language.

Start with the use case and maturity level

The following table describes the UE5.8 baseline. Standard means the source documentation does not explicitly label the feature Beta or Experimental; it is not a promise that a project needs no testing.

Use case Main option Maturity and constraints Adoption guidance
HUDs, menus, and dialogs UMG Standard The default choice. Keep gameplay state and lifetime management outside the Widget Blueprint.
Layered menus, Back navigation, and gamepad support UMG + CommonUI The release notes describe Common UI itself as production-ready; the Enhanced Input how-to retains older warnings. Distinguish the current integration path from the legacy path and test the one you use.
Many displayed fields or state shared across screens Manual events / Presenter; MVVM when needed UMG Viewmodel is Beta. Do not introduce MVVM merely because a small screen has a few values. Add regression tests when adopting it.
Overhead health bars, terminals, and VR panels Widget Component + UMG Standard; requires world rendering and pointer design. Measure widget count, Draw Size, update frequency, occlusion, and interaction.
Editor production tools Editor Utility Widget Beta Move heavy asset processing into a C++ editor module.
Editor plugins and low-level UI components Slate Standard C++-oriented. Do not rebuild every ordinary game screen in Slate.
Crosshairs and immediate-mode diagnostic drawing AHUD Canvas Standard Suitable for simple drawing and debugging, not a general menu framework.
Research and diagnostic UI UIFramework / SlateIM Experimental Not the default choice for a shipping product.

The maturity labels are documented in Epic's references for Editor Utility Widgets, UMG Viewmodel, UIFramework, and SlateIM. The CommonUI documentation discrepancy is discussed below with links to both sources.

When the choice is unclear, start with UMG. Add CommonUI for screen stacks and focus, a Presenter or MVVM for complex presentation, and Slate only for low-level components that are otherwise missing.

Separate the data path from the control path

A Presenter or Viewmodel produces display data. A ULocalPlayerSubsystem-based UI manager owns screens and coordinates stacks and input. These are different responsibilities, not interchangeable names for the same layer.

Data, simple:
  State owner --event--> Screen owner / thin adapter --> Widget

Data, complex:
  State owner --event--> Presenter / Viewmodel --> Widget

Control:
  PlayerController / HUD / LocalPlayer UI manager
    --create, retain, push, pop--> Widget
  CommonUI Action Router / PlayerController
    --input policy, Back, focus, mouse capture--> Widget
Enter fullscreen mode Exit fullscreen mode

In the ammunition example later in this article, the PlayerController also acts as a minimal presentation adapter: it forwards values to the widget. Extract a dedicated Presenter or Viewmodel when formatting, multiple data sources, asynchronous state, shared presentation, or isolated tests make that forwarding layer more complex.

Choose an owner by lifetime and player scope

Owner Lifetime and scope Main responsibility and when to choose it
APlayerController Represents one controlling player and generally outlives a Pawn. Behavior across map travel depends on the travel mechanism. A few HUD widgets, the Owning Player relationship, and reconnection after Pawn changes. Appropriate while the responsibility remains small.
AHUD Tied to the PlayerController/world-side lifetime; can span Pawn changes. HUD coordination and Canvas drawing, especially when the existing architecture already uses the HUD as its entry point.
ULocalPlayerSubsystem-based UI manager Shares the LocalPlayer's lifetime, outlives individual Pawns, and follows that LocalPlayer across world changes. Layers, stacks, and input coordination. Useful for multiple screens and split-screen play.
GameInstance / GameViewport-level owner Application-wide or scoped to a GameInstance rather than one local player. Loading screens, global notifications, and player-independent overlays.
Actor + Widget Component Follows the Actor/world lifetime. Overhead indicators, in-world terminals, and VR panels.

ULocalPlayerSubsystem shares its lifetime with ULocalPlayer and exposes PlayerControllerChanged. See the ULocalPlayerSubsystem API. A persistent manager does not make a widget's references to an old world safe to retain: recreate and reconnect world-dependent widgets when the map changes.

Create player-specific UI with a valid Owning Player and add it with UUserWidget::AddToPlayerScreen. Decide who owns the screen separately from who owns the gameplay data shown on it.

Choosing among the UI features

UMG: the default for game screens

UMG is the starting point for HUDs, title screens, pause menus, inventories, shops, and conversations.

A Widget Blueprint should not become the owner of saving, networking, purchases, game progression, or a per-frame search for gameplay state. Keep that state elsewhere and pass the widget the data it needs to display.

CommonUI: layered interaction on top of UMG

CommonUI adds input routing, Activatable Widgets, styling, and input glyphs to UMG. It is useful for switching input devices, managing menu layers, handling Back, and restoring focus. See the Common UI Overview. Use Activatable Widgets for screens that participate in the interaction flow, rather than making every decorative widget activatable.

UCommonActivatableWidget can activate and deactivate, but it does not automatically activate or register as a Back handler by default. Enable Auto Activate or activate it explicitly, configure Back Handler where appropriate, and provide a Desired Focus Target. RequestRefreshFocus restores focus only when the widget is the leaf-most active node. See the Activatable Widget API and RequestRefreshFocus API.

UE5.8 CommonUI and Enhanced Input: the documentation is inconsistent

The UE5.8 release notes describe the integration of Enhanced Input with Common Input/UI, a reduction in duplicated Data Assets, reliability and debugging improvements, and Common UI itself becoming production-ready.

Meanwhile, the Enhanced Input integration how-to retains Experimental and limited support wording, together with a shipping warning originating from UE5.2. Do not use either page alone to make an unconditional claim about stability or shipping suitability.

Keep the two configuration paths distinct:

  • Current path: Input Actions, Player Mappable Key Settings, metadata implementing ICommonMappingContextMetadataInterface, and Input Mapping Contexts (IMCs). The standard metadata class is UCommonMappingContextMetadata.
  • Legacy path: A DataTable using CommonInputActionDataBase as its row structure, together with row handles.
  • Shared asset does not mean legacy: CommonUIInputData can also reference the current Accept/Back Input Actions. Its presence alone does not identify which path a project uses.
  • Migration rule: Choose one authoritative registration path for each action. Do not register the same action through both paths.

When Is Generic Input Action is enabled, CommonUI does not fire the ordinary Enhanced Input event for that action. Also check for duplicate registration between the CommonUI layer and gameplay input. See the CommonUI Input Technical Guide.

Slate, Canvas, and Widget Components

Slate is appropriate for editor plugins and low-level components that UMG does not provide. It is not a requirement merely because a project uses C++. See the Slate UI Framework.

AHUD Canvas is useful for crosshairs and diagnostic drawing. It is not a replacement for a menu architecture.

UWidgetComponent requires explicit decisions about draw size, redraw policy, distance, occlusion, and how many widgets update. See Widget Components. For UWidgetInteractionComponent, allocate Virtual User and Pointer indices deliberately so independent players or VR hands do not collide. See Widget Interaction Components.

Beta and Experimental features

Editor Utility Widgets and UMG Viewmodel are Beta. UIFramework and SlateIM are Experimental. Keep those labels visible in the adoption decision, not just in a footnote. For editor tooling, move heavy processing into a C++ editor module; treat Experimental UI features as research or diagnostic options rather than the default production foundation. The corresponding feature references are linked in the opening table.

Prerequisites for a new UE5.8 project

Adding modules to Build.cs does not enable plugins. Enable the plugins through the Plugins window or the project's .uproject file.

The following is a Plugins fragment to merge into an existing .uproject file, not a complete project file. Enable only the features the project actually uses: plain UMG does not require CommonUI or MVVM.

{"Plugins":[
  {"Name":"CommonUI","Enabled":true},
  {"Name":"EnhancedInput","Enabled":true},
  {"Name":"ModelViewViewModel","Enabled":true}
]}
Enter fullscreen mode Exit fullscreen mode

CommonUI setup order

  1. Enable CommonUI and, when required, EnhancedInput.
  2. Set Game Viewport Client Class to CommonGameViewportClient or a subclass.
  3. Assign Input Data and platform-specific Controller Data in Common Input Settings.
  4. For the current integration path, enable Enhanced Input Support, configure the Input Action metadata, and register the actions in an IMC.
  5. Reference the current Accept/Back Input Actions from CommonUIInputData.
  6. When retaining legacy configuration, avoid registering the same action through both the DataTable and current paths.
  7. Test keyboard/mouse, gamepad, device switching, and the complete pause → modal → gameplay sequence on the target hardware.

The viewport client forwards input into the CommonUI router and lets unhandled input continue toward gameplay. Keeping an unconditional duplicate action handler in the PlayerController can cause the same action to fire twice. See the CommonUI Input Technical Guide.

MVVM creation types

Enable the UMG Viewmodel plugin, then choose how the Viewmodel is supplied in the Widget Designer's Viewmodels panel. See UMG Viewmodel.

Creation type Purpose
Create Instance Automatically create a Viewmodel for each widget.
Manual Inject an existing instance created by the owner or Presenter.
Global Viewmodel Collection Retrieve an instance from the MVVM Subsystem's shared collection.
Property Path Resolve an instance through a property/function path.
Resolver Delegate acquisition or creation to a resolver object.
Context Obtain the Viewmodel from a local context provider. A parent widget binding or external Context Extension can supply it; manual assignment is also possible.

The UE5.8 enum includes Resolver and Context, not just the four older options. See the Creation Type API and UE5.8 release notes. Shared context resolution is defined through UMVVMViewModelContextResolver. These remain part of the Beta MVVM feature and need regression coverage.

Build.cs dependencies

Merge these lines into the existing module rules constructor; they are not a complete Build.cs file.

PublicDependencyModuleNames.AddRange(new[] { "Core", "CoreUObject", "Engine", "UMG" });

// Add only when UMyUIManager.cpp directly uses FSlateApplication / FSlateUser.
PrivateDependencyModuleNames.AddRange(new[] { "Slate", "SlateCore" });
Enter fullscreen mode Exit fullscreen mode

Add only the modules whose C++ types the project actually uses. CommonUI classes require CommonUI; Common Input types require CommonInput; Enhanced Input types require EnhancedInput; and MVVM APIs require ModelViewViewModel.

FKey and related input types require InputCore. Direct Slate usage, such as SWidget, FReply, or the focus APIs used below, requires the appropriate Slate / SlateCore dependencies. Dependencies exposed through public headers belong in PublicDependencyModuleNames; implementation-only dependencies belong in PrivateDependencyModuleNames.

The split-screen-aware RestoreGameplayInput() example directly uses FSlateApplication and FSlateUser in a .cpp file, so that example requires Slate and SlateCore as private dependencies. They are not blanket prerequisites for every plain UMG project.

Use the same responsibility model in Blueprint and C++

Responsibility Blueprint-first project C++-first project
State ownership Actor Component, PlayerState, or Subsystem Native class, service, or Gameplay Ability
Presentation Blueprint Presenter or Viewmodel; the owner for a simple screen UObject Presenter or native Viewmodel; the owner for a simple screen
Widget ownership BP_PlayerController, BP_HUD, or a UI manager exposed to Blueprint through C++ when needed PlayerController, HUD, or a ULocalPlayerSubsystem subclass
Input routing Enhanced Input, CommonUI, and BP_UIManager CommonUI Action Router, PlayerController, and UI manager

A Blueprint-first ammunition HUD

The same architecture can be implemented without moving every responsibility into native code:

  1. Expose the source contract. BP_AmmoComponent provides getters, a RequestReload command, and an OnAmmoChanged Event Dispatcher.
  2. Create and retain the widget. BP_PlayerController or BP_UIManager creates WBP_AmmoHUD with an Owning Player, stores it in a variable, and calls Add to Player Screen.
  3. Track Pawn changes. The owner binds once to On Possessed Pawn Changed, immediately synchronizes with Get Controlled Pawn, and calls WBP_AmmoHUD.InitializeAmmoHUD(AmmoComponent) whenever the Pawn changes—including None and spectator transitions.
  4. Reconnect explicitly. InitializeAmmoHUD unbinds the same Custom Event from the previously retained component, stores the new component, binds again, and immediately reads the getters. With None, reset the display rather than keeping the previous Pawn's ammunition.
  5. Close symmetrically. CloseAmmoHUD unbinds that same Custom Event. Do not use Unbind All and accidentally remove other listeners.
  6. Send commands instead of setting state. The reload button calls the retained component, or uses BPI_AmmoCommands to reach a Subsystem command. It does not directly overwrite ammunition values.

Choose the binding pair that matches the screen's lifetime: Construct/Destruct for removal and re-addition, Activated/Deactivated for CommonUI interaction, or explicit Open/Close for a screen that only changes visibility. Reopening must reconnect and immediately synchronize the current values.

A Blueprint Interface defines a command/getter boundary; it does not replace an Event Dispatcher. In a networked game, connect the command to the appropriate Server RPC or Gameplay Ability rather than assuming that a UI call has gameplay authority.

When to move a responsibility into C++

Move code when there is a concrete benefit: shared native type contracts, reuse, reviewable text diffs, automated tests, asynchronous or networking boundaries, or a measured processing bottleneck. This is not a claim that Blueprint lacks type checking.

“Blueprint is slow” is not a sufficient reason by itself. Layout, styles, animations, and hover/focus feedback can remain in Widget Blueprints while state and lifecycle contracts move into C++ incrementally.

C++ example: connect creation, ownership, initial state, and cleanup

The ammunition HUD has three parts:

  • UAmmoSourceComponent owns the state-facing contract.
  • UAmmoWidget renders the values and emits a reload request.
  • AMyPlayerController owns the widget and acts as the thin presentation adapter.

Replace MYGAME_API with your module's API macro. The source component below defines the public contract used by the controller; it is not a complete replicated weapon system.

1. Define the source component contract

The component declares the native delegate type, a reference-returning delegate getter, value getters, a state-change broadcast, and RequestReload().

Weapon calculations, replication, and server validation are deliberately outside this minimal example. A BlueprintNativeEvent places its default C++ implementation in _Implementation; see UFunctions. Here, reloading intentionally does nothing until a Blueprint override or C++ subclass forwards the request to a Server RPC, Ability, or weapon system.

// AmmoSourceComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "AmmoSourceComponent.generated.h"

DECLARE_MULTICAST_DELEGATE_TwoParams(FOnAmmoChangedNative, int32, int32);

UCLASS(ClassGroup=(Combat), meta=(BlueprintSpawnableComponent))
class MYGAME_API UAmmoSourceComponent : public UActorComponent
{
    GENERATED_BODY()
public:
    FOnAmmoChangedNative& OnAmmoChanged() { return AmmoChanged; }
    UFUNCTION(BlueprintPure, Category="Ammo") int32 GetCurrentAmmo() const
    { return FMath::Clamp(CurrentAmmo, 0, GetMagazineSize()); }
    UFUNCTION(BlueprintPure, Category="Ammo") int32 GetMagazineSize() const
    { return FMath::Max(MagazineSize, 1); }
    UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Ammo")
    void RequestReload();
    virtual void RequestReload_Implementation();
    void ApplyAmmoStateFromGameplay(int32 NewCurrent, int32 NewMagazine);

private:
    UPROPERTY(Transient) int32 CurrentAmmo = 30;
    UPROPERTY(EditDefaultsOnly, Category="Ammo", meta=(ClampMin="1"))
    int32 MagazineSize = 30;
    FOnAmmoChangedNative AmmoChanged;
};
Enter fullscreen mode Exit fullscreen mode
// AmmoSourceComponent.cpp
#include "AmmoSourceComponent.h"

void UAmmoSourceComponent::RequestReload_Implementation()
{
    // In production, forward to a Server RPC, Ability, or weapon system.
}

void UAmmoSourceComponent::ApplyAmmoStateFromGameplay(
    int32 NewCurrent, int32 NewMagazine)
{
    const int32 Magazine = FMath::Max(NewMagazine, 1);
    const int32 Current = FMath::Clamp(NewCurrent, 0, Magazine);
    if (CurrentAmmo == Current && MagazineSize == Magazine) return;
    CurrentAmmo = Current;
    MagazineSize = Magazine;
    AmmoChanged.Broadcast(CurrentAmmo, MagazineSize);
}
Enter fullscreen mode Exit fullscreen mode

This native delegate is not a UPROPERTY. A subscriber retains its FDelegateHandle and removes it from the same delegate instance when disconnecting. Read the getters for the initial display; do not wait for the next change notification.

2. Define the display widget

In the derived Widget Blueprint, connect the button's OnClicked event to the widget's RequestReload function. The widget emits an intent; the controller forwards it to the current source.

// AmmoWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/TextBlock.h"
#include "AmmoWidget.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAmmoReloadClicked);

UCLASS(Abstract)
class MYGAME_API UAmmoWidget : public UUserWidget
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, Category="HUD")
    void SetAmmo(int32 Current, int32 Magazine)
    {
        if (!ensure(AmmoText)) return;
        const int32 SafeMagazine = FMath::Max(Magazine, 1);
        Current = FMath::Clamp(Current, 0, SafeMagazine);
        AmmoText->SetText(FText::Format(
            NSLOCTEXT("AmmoWidget", "AmmoFormat", "{0} / {1}"),
            FText::AsNumber(Current), FText::AsNumber(SafeMagazine)));
    }
    UFUNCTION(BlueprintCallable, Category="HUD")
    void RequestReload() { OnReloadClicked.Broadcast(); }
    UPROPERTY(BlueprintAssignable, Category="HUD")
    FOnAmmoReloadClicked OnReloadClicked;

protected:
    UPROPERTY(meta=(BindWidget)) TObjectPtr<UTextBlock> AmmoText;
};
Enter fullscreen mode Exit fullscreen mode

Add the required Text Block named AmmoText to the derived Widget Blueprint. Its name and type must satisfy the BindWidget declaration. See BindWidget metadata.

3. Own the HUD in the PlayerController and use one Pawn-change path

Do not use separate OnPossess, AcknowledgePossession, and OnUnPossess overrides just to reconnect the HUD. OnPossessedPawnChanged is notified on both authority and client, and either the old or new Pawn can be nullptr. See the AController API and FOnPossessedPawnChanged API.

// MyPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MyPlayerController.generated.h"

class APawn;
class UAmmoSourceComponent;
class UAmmoWidget;

UCLASS()
class MYGAME_API AMyPlayerController : public APlayerController
{
    GENERATED_BODY()
protected:
    virtual void BeginPlay() override;
    virtual void EndPlay(const EEndPlayReason::Type Reason) override;
private:
    UPROPERTY(EditDefaultsOnly, Category="UI")
    TSubclassOf<UAmmoWidget> AmmoWidgetClass;
    UPROPERTY(Transient) TObjectPtr<UAmmoWidget> AmmoWidget;
    TWeakObjectPtr<UAmmoSourceComponent> AmmoSource;
    FDelegateHandle AmmoChangedHandle;
    UFUNCTION() void HandlePawnChanged(APawn* OldPawn, APawn* NewPawn);
    UFUNCTION() void HandleReload();
    void BindAmmoHudToPawn(APawn* NewPawn);
    void UnbindAmmoSource();
    void HandleAmmoChanged(int32 Current, int32 Magazine);
};
Enter fullscreen mode Exit fullscreen mode
// MyPlayerController.cpp
#include "MyPlayerController.h"
#include "AmmoSourceComponent.h"
#include "AmmoWidget.h"
#include "GameFramework/Pawn.h"

void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();
    if (!IsLocalController() || !ensure(AmmoWidgetClass)) return;
    AmmoWidget = CreateWidget<UAmmoWidget>(this, AmmoWidgetClass);
    if (!ensure(AmmoWidget)) return;

    AmmoWidget->OnReloadClicked.AddUniqueDynamic(
        this, &AMyPlayerController::HandleReload);
    AmmoWidget->AddToPlayerScreen();
    OnPossessedPawnChanged.AddUniqueDynamic(
        this, &AMyPlayerController::HandlePawnChanged);
    BindAmmoHudToPawn(GetPawn());
}

void AMyPlayerController::HandlePawnChanged(APawn* OldPawn, APawn* NewPawn)
{
    (void)OldPawn;
    BindAmmoHudToPawn(NewPawn);
}

void AMyPlayerController::BindAmmoHudToPawn(APawn* NewPawn)
{
    UnbindAmmoSource();
    UAmmoSourceComponent* Source = NewPawn
        ? NewPawn->FindComponentByClass<UAmmoSourceComponent>() : nullptr;
    AmmoSource = Source;
    if (!Source) { HandleAmmoChanged(0, 1); return; }

    AmmoChangedHandle = Source->OnAmmoChanged().AddUObject(
        this, &AMyPlayerController::HandleAmmoChanged);
    HandleAmmoChanged(Source->GetCurrentAmmo(), Source->GetMagazineSize());
}

void AMyPlayerController::UnbindAmmoSource()
{
    if (UAmmoSourceComponent* Source = AmmoSource.Get();
        Source && AmmoChangedHandle.IsValid())
        Source->OnAmmoChanged().Remove(AmmoChangedHandle);
    AmmoChangedHandle.Reset();
    AmmoSource.Reset();
}

void AMyPlayerController::HandleAmmoChanged(int32 Current, int32 Magazine)
{
    if (AmmoWidget) AmmoWidget->SetAmmo(Current, Magazine);
}

void AMyPlayerController::HandleReload()
{
    if (UAmmoSourceComponent* Source = AmmoSource.Get()) Source->RequestReload();
}

void AMyPlayerController::EndPlay(const EEndPlayReason::Type Reason)
{
    OnPossessedPawnChanged.RemoveDynamic(
        this, &AMyPlayerController::HandlePawnChanged);
    UnbindAmmoSource();
    if (AmmoWidget)
    {
        AmmoWidget->OnReloadClicked.RemoveDynamic(
            this, &AMyPlayerController::HandleReload);
        AmmoWidget->RemoveFromParent();
        AmmoWidget = nullptr;
    }
    Super::EndPlay(Reason);
}
Enter fullscreen mode Exit fullscreen mode

Configure a concrete Widget Blueprint derived from UAmmoWidget as AmmoWidgetClass, use the controller class in the game configuration, and give the relevant Pawn a UAmmoSourceComponent or subclass. Gameplay must feed state changes into the source; the example does not add replication for you.

BeginPlay binds once to the Pawn-change notification and immediately synchronizes using GetPawn(). Every reconnection first unsubscribes from the old source. A null Pawn—or a Pawn without the source component—resets the HUD to 0 / 1, the example's explicit no-source display.

Environment Transition Acceptance condition
Listen server's local player Pawn A → nullptr / spectator → Pawn B Pawn A notifications stop immediately; the no-source state resets the HUD; each Pawn B event produces exactly one update.
Remote owning client Pawn A → nullptr / spectator → Pawn B No old subscription or stale Pawn A display remains on the client; each Pawn B event produces exactly one update.

The controller owns this HUD until EndPlay. A design that temporarily closes the HUD needs its own matching disconnect/reconnect contract; the lifecycle choices in the next section explain how to define one.

Extract a dedicated class such as UAmmoHudPresenter when the adapter must combine multiple sources, share formatted text, handle asynchronous loading or replays, or support tests without a live gameplay source.

Delegate subscriptions need four distinct lifetime contracts

Binding in OnInitialized and unbinding in Destruct is an unsafe pairing for a reusable widget instance. After removal and re-addition, OnInitialized does not run again to restore that subscription.

OnInitialized normally runs once per instance; Construct and Destruct can run multiple times as the underlying widget hierarchy is constructed and destroyed. See OnInitialized, Construct, and Destruct.

Lifetime contract Bind / unbind pair Typical use
Entire widget instance Bind once in NativeOnInitialized; normally do not remove it in NativeDestruct. A callback from a child button within the same widget.
Presence in the widget hierarchy NativeConstruct / NativeDestruct External gameplay delegates, timers, or message-bus subscriptions tied to that presence.
Active CommonUI interaction NativeOnActivated / NativeOnDeactivated Subscriptions and input needed only while the screen is active.
Explicit visibility-based open period Explicit Open() / Close() A panel that becomes Hidden or Collapsed without being removed.

NativePreConstruct also runs for Designer previews, so do not access world state there unconditionally.

Test all four transitions: first display, remove → re-add, deactivate → reactivate, and hide → show. In each case, reconnecting should synchronize immediately, necessary notifications should not disappear, and each event should be delivered exactly once.

Restore input after closing the last CommonUI screen

FUIInputConfig controls input mode, mouse capture/lock, bHideCursorDuringViewportCapture, and whether move/look input is ignored. That cursor flag means hide the cursor while the viewport has capture. General visibility policy through APlayerController::bShowMouseCursor is a separate responsibility. See the FUIInputConfig API.

When no desired configuration is supplied, CommonUI can fall back to the last valid input configuration. Deactivating every Activatable Widget does not necessarily restore gameplay input. See Input Fundamentals for CommonUI.

Use one of two explicit policies:

  1. Keep a persistent gameplay Activatable Widget at the root. It returns ECommonInputMode::Game, the intended mouse-capture policy, and enabled move/look input.
  2. When removing all UI, let the LocalPlayer UI manager restore gameplay input configuration, bShowMouseCursor, and viewport focus after the final pop completes.

The following is a method excerpt for a UGameplayRoot class derived from UCommonActivatableWidget; declare the matching override in that class.

TOptional<FUIInputConfig> UGameplayRoot::GetDesiredInputConfig() const
{
    FUIInputConfig C(ECommonInputMode::Game,
        EMouseCaptureMode::CapturePermanently_IncludingInitialMouseDown,
        true); // Hide the cursor while the viewport has capture.
    C.bIgnoreMoveInput = false;
    C.bIgnoreLookInput = false;
    return C;
}
Enter fullscreen mode Exit fullscreen mode

The next excerpt belongs to a UMyUIManager derived from ULocalPlayerSubsystem, with RestoreGameplayInput() declared in its class. It restores viewport focus only for the Slate User associated with that LocalPlayer. Include the CommonUI module and the private Slate dependencies described earlier.

#include "Engine/LocalPlayer.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Application/SlateUser.h"
#include "GameFramework/PlayerController.h"
#include "Input/CommonUIActionRouterBase.h"
#include "Input/UIActionBindingHandle.h"

void UMyUIManager::RestoreGameplayInput()
{
    ULocalPlayer* LocalPlayer = GetLocalPlayer();
    if (!LocalPlayer) return;

    UCommonUIActionRouterBase* Router =
        LocalPlayer->GetSubsystem<UCommonUIActionRouterBase>();
    if (!Router) return;

    FUIInputConfig C(ECommonInputMode::Game,
        EMouseCaptureMode::CapturePermanently_IncludingInitialMouseDown, true);
    C.bIgnoreMoveInput = C.bIgnoreLookInput = false;
    Router->SetActiveUIInputConfig(C, this);

    if (APlayerController* PC = LocalPlayer->GetPlayerController(GetWorld()))
    {
        PC->bShowMouseCursor = false;
    }

    if (TSharedPtr<FSlateUser> SlateUser = LocalPlayer->GetSlateUser())
    {
        FSlateApplication::Get().SetUserFocusToGameViewport(
            SlateUser->GetUserIndex(),
            EFocusCause::SetDirectly);
    }
}
Enter fullscreen mode Exit fullscreen mode

ULocalPlayer::GetSlateUser() returns the Slate User associated with that LocalPlayer. FSlateApplication exposes separate APIs for all users, SetAllUserFocusToGameViewport, and for one specified user, SetUserFocusToGameViewport. See the FSlateApplication API.

Do not replace the user-specific call with UWidgetBlueprintLibrary::SetFocusToGameViewport() in a split-screen UI manager. That Blueprint utility has no LocalPlayer parameter; the UE5.8 source behavior identified during review delegates to SetAllUserFocusToGameViewport(). Restrict it to projects that guarantee exactly one LocalPlayer. See the UWidgetBlueprintLibrary API. In a Blueprint-first split-screen project, expose the user-specific operation through a BlueprintCallable C++ wrapper.

UCommonUIActionRouterBase itself derives from ULocalPlayerSubsystem and provides SetActiveUIInputConfig; see its API reference. Use RequestRefreshFocus only with the leaf-most active node, as required by its API contract.

Step Keyboard / mouse Gamepad
Open pause Move/look stop; cursor and capture match the menu policy. The initial button receives focus and is usable.
Open a modal Only the modal can be operated. Back closes exactly one layer.
Close the modal Control returns to the pause menu. Focus returns inside the pause menu.
Close pause Viewport focus, capture, and move/look are restored. No stale UI focus consumes gameplay input; each input reaches its intended gameplay target once.

The split-screen acceptance test is stronger than “input works for both players.” Close Player 0's pause screen while Player 1 keeps a menu open: only Player 0 returns to the viewport, and Player 1 retains the menu and its focus. Repeat with the players reversed. Both directions must preserve one delivery per input to the correct target.

Manual events, MVVM, and performance

When there are few screens and ownership is clear, manual delegates or Event Dispatchers are enough. Consider a Presenter or MVVM when many widgets share the same state, display transformations need reuse, or Designer bindings and two-way binding provide a concrete benefit.

The following is a Viewmodel method excerpt, not a complete Viewmodel class. It assumes a CurrentHealth field and GetHealthText function configured for FieldNotify, as described in the UMG Viewmodel documentation.

void UPlayerStatusViewModel::SetCurrentHealth(int32 NewValue)
{
    if (UE_MVVM_SET_PROPERTY_VALUE(CurrentHealth, FMath::Max(NewValue, 0)))
        UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthText);
}
Enter fullscreen mode Exit fullscreen mode

A Viewmodel mediates display state and commands. It should not become the owner of damage calculations, persistent inventory storage, or server validation.

For UI performance, update frequency is often a more useful first question than whether a function was written in Blueprint or C++. Avoid per-frame casts, raw property bindings, repeated SetText calls with unchanged values, and unnecessary subscriptions for hidden screens. Prefer change events, List View entry reuse, invalidation, and reduced update frequency where appropriate. Apply a Retainer Box after measurement, not as a universal fix. See Optimization Guidelines for UMG and Invalidation in Slate and UMG.

A reproducible UI test matrix

Record aspect ratio, resolution, input device, world/VR interaction, and accessibility as separate dimensions. The values below are representative test conditions; replace the minimums with the actual supported minimums for your product and record the target platform for each run.

Dimension Representative conditions Acceptance condition
16:9 Minimum 1280×720; also 1920×1080 and 3840×2160 No clipping, overlap, or inaccessible controls.
16:10 / 21:9 / 32:9 1280×800, 3440×1440, and 5120×1440 when supported Anchors, Safe Zones, and maximum widths remain within the design specification.
DPI and language Minimum/maximum scaling, English/Japanese, longest translation, maximum digit count No clipping, overlap, focus loss, or unstable layout.
Keyboard / mouse Click, Tab, Escape, and pause → modal → gameplay Focus, capture, cursor visibility, and gameplay input are correct; no double firing.
Gamepad D-pad/stick, Accept, Back, and device switching Initial/restored focus is correct; Back closes one layer.
Touch Tap, scroll, software keyboard, and safe areas Touch targets remain usable; scrolling does not conflict with other controls; the keyboard does not obscure required interaction.
Split-screen Two LocalPlayers. Player 0 closes pause while Player 1 keeps a menu open; repeat in reverse. Only the closing player's focus returns to the viewport. The other player's menu and focus remain unchanged. Each user's input reaches the correct target exactly once.
Pawn transitions Pawn A → null / spectator → Pawn B Unsubscribe from A, reset with no source, and update once per B event.
Widget lifetime First display, remove/re-add, deactivate/reactivate, and hide/show No missing or duplicated notifications.
World UI / VR Occlusion, distance, left/right hands, and ray hit/miss Unnecessary updates stop; Virtual User / Pointer assignments do not collide.
Accessibility Keyboard navigation, contrast, and screen readers on supported platforms Reading order, labels, and control reachability match the accessibility specification.

Automation Driver primarily targets desktop keyboard/mouse-style interaction. Test gamepads, touch, and motion controllers on hardware or with a separate harness rather than assuming Automation Driver covers them. See Automation Driver.

Screen Reader support is Experimental. The documented platform scope includes third-party screen readers on Windows and VoiceOver on iOS; do not assume universal platform coverage. See Supporting Screen Readers. Use Widget Reflector and Slate Insights to inspect focus, hit testing, invalidation, and updates.

Final recommendations

Use UMG for ordinary screens, CommonUI for screen stacks, Back navigation, and focus, a Presenter or Beta MVVM for complex presentation, and Widget Components for world-space UI.

Editor Utility Widgets and UMG Viewmodel are Beta; UIFramework and SlateIM are Experimental. The release notes describe Common UI itself as production-ready, but the Enhanced Input how-to retains older warnings. Keep the current and legacy integration paths separate, and validate the configuration used by the project against the linked documentation and target build.

Whether the project is Blueprint-first or C++-first, make the same four responsibilities explicit: state ownership, presentation, widget ownership, and input routing. Once those boundaries are clear, the Blueprint/C++ split can evolve without rewriting the whole UI architecture.

References

Version-sensitive and API-specific claims link directly to the relevant Epic documentation throughout the article. This English adaptation retains the UE5.8 scope of the reviewed Japanese edition. That edition records August 31, 2026 as its technical reference date; the translation is not a new engine build or runtime validation.

Top comments (1)

Collapse
 
lewisywliu profile image
Lewis Liu •

This is one of the few writeups I have seen that gets the CommonUI input restoration path right, the GetDesiredInputConfig detail is exactly the kind of thing that is buried in release notes and nowhere else.

Two small additions from the sidelines: BindWidgetOptional is worth mentioning next to BindWidget, it saves you from crashes when a derived layout drops a widget the base class expects. And layout invalidation deserves as much attention as update frequency: a single SetText on a widget deep inside auto-sized Fill panels can walk the whole chain and re-run layout, while the same text in a fixed-size slot is nearly free. Moving live numbers out of deep auto-layout hierarchies usually buys more than any Blueprint to C++ port.

Out of curiosity, since you flag the docs inconsistency around CommonUI input: did the activatable stack navigation hold up on gamepad with nested screens in 5.8, or did you end up wiring custom back behavior anyway? Most public postmortems stop at a single menu layer so there is almost nothing to compare against.