Skip to main content

🖼️ Lesson 9.1: UserWidget & UMG in C++

HUDs, menus, health bars — game UI is built with UMG. Designers lay out widgets visually in a Widget Blueprint; your C++ drives the logic behind them. This lesson wires the two together with the same base-class-plus-subclass pattern you mastered for weapons in Module 6.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Create a UUserWidget subclass in C++ and pair it with a Widget Blueprint
  • Bind C++ pointers to designer-placed widgets with meta = (BindWidget)
  • Create a widget at runtime and add it to the viewport
  • Use NativeConstruct as the widget's init hook
  • Add the UMG module dependency

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

The UMG Split

UMG has two halves that mirror the C++/Blueprint division you already know:

graph LR WBP["Widget Blueprint (WBP_HUD)
visual layout: text, bars, buttons"] -->|"is a subclass of"| CPP["UMyHUDWidget (C++)
logic: update values, handle clicks"] CPP -->|"BindWidget pointers"| WBP
Figure 1: A Widget Blueprint holds the visual layout; a C++ UUserWidget base holds the logic. BindWidget connects C++ pointers to the widgets the designer placed — the same base/subclass split as Lesson 6.4.

📖 Why not build the layout in C++?

You can construct widgets entirely in code (via Slate), but it's verbose and non-visual. The idiomatic approach is: lay out visually in a Widget Blueprint, drive with C++. Designers arrange and style; you bind to the pieces you need and control behavior. It's the hybrid model (Lesson 1.1) applied to UI.

Module & Base Class

UMG lives in its own module — add it to .Build.cs (Lesson 2.1/8.1) or nothing will compile:

// MyProject.Build.cs
PublicDependencyModuleNames.AddRange(new string[]
{ "Core", "CoreUObject", "Engine", "InputCore", "UMG" });   // ← add UMG

Then a UUserWidget subclass. Create via New C++ Class → User Widget:

// MyHUDWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MyHUDWidget.generated.h"

class UProgressBar;
class UTextBlock;

UCLASS(Abstract)   // Abstract: use it via a Widget Blueprint subclass (Lesson 6.4)
class MYPROJECT_API UMyHUDWidget : public UUserWidget
{
    GENERATED_BODY()

protected:
    virtual void NativeConstruct() override;   // the widget's "BeginPlay"
};

✅ Same pattern, new class

Mark the C++ widget Abstract and create a Widget Blueprint subclass of it (right-click the C++ class → but for widgets you reparent a WBP, or create the WBP with your class as its parent). The designer builds layout in the WBP; your C++ base holds logic. If this feels familiar, it's Lesson 6.4's AWeaponBase pattern applied to UI.

BindWidget

Here's the connective tissue. A designer places a Progress Bar named HealthBar and a Text Block named AmmoText in the Widget Blueprint. In C++, you declare pointers with meta = (BindWidget), and Unreal automatically links them by name at construction.

// MyHUDWidget.h — add bound widget pointers.
protected:
    // The name MUST match the widget's name in the Widget Blueprint.
    UPROPERTY(meta = (BindWidget))
    TObjectPtr<UProgressBar> HealthBar;

    UPROPERTY(meta = (BindWidget))
    TObjectPtr<UTextBlock> AmmoText;

    // Optional binding — no error if the WBP lacks it:
    UPROPERTY(meta = (BindWidgetOptional))
    TObjectPtr<UTextBlock> ScoreText;

⚠️ Name match is mandatory (and case-sensitive)

With BindWidget, the C++ property name must exactly match the widget's name in the Widget Blueprint. If they don't match, the Blueprint won't compile — it errors that a required bound widget is missing. Use BindWidgetOptional when a widget might not exist in every subclass. This name-based binding is the reflection system again (Lesson 2.2) — it links by name at runtime.

📖 Now you have typed access

Once bound, HealthBar and AmmoText are ordinary pointers into the designer's layout. You call their API — HealthBar->SetPercent(...), AmmoText->SetText(...) — from C++, with full type safety. The designer owns where the bar is and how it looks; you own what value it shows.

Creating & Showing a Widget

A widget doesn't appear until you create an instance and add it to the viewport. This is typically done from the PlayerController (Lesson 4.3 — UI belongs to the player).

// In the PlayerController header:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI")
TSubclassOf<UMyHUDWidget> HUDWidgetClass;   // designer assigns WBP_HUD

UPROPERTY()
TObjectPtr<UMyHUDWidget> HUDWidget;          // the live instance (GC-safe, Lesson 2.5)
// In the PlayerController .cpp:
void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();

    if (HUDWidgetClass)
    {
        // CreateWidget makes the instance; 'this' controller is its owner.
        HUDWidget = CreateWidget<UMyHUDWidget>(this, HUDWidgetClass);
        if (HUDWidget)
        {
            HUDWidget->AddToViewport();   // now it's on screen
        }
    }
}

✅ TSubclassOf again

TSubclassOf<UMyHUDWidget> (Lesson 3.4/6.3) lets the designer assign WBP_HUD in the editor — code doesn't hardcode which widget asset to use. CreateWidget instantiates it, AddToViewport shows it. Store the instance as a UPROPERTY so the GC keeps it alive and you can update or remove it later.

⚠️ Remove it when done

A widget added to the viewport stays until removed. Call HUDWidget->RemoveFromParent() when the UI should disappear (game over, menu closed). Leaving orphaned widgets on screen — or recreating them without removing the old ones — is a common UI leak.

Widget Lifecycle

Widgets have their own lifecycle hooks, analogous to an actor's (Lesson 3.2):

HookRuns whenUse for
NativeConstructThe widget is constructed / addedInit, subscribe to events, first values (like BeginPlay)
NativeDestructThe widget is removed/destroyedUnsubscribe, cleanup (like EndPlay)
NativeTickEvery frame (if enabled)Rarely — prefer events (next lesson)
void UMyHUDWidget::NativeConstruct()
{
    Super::NativeConstruct();   // always call Super (Lesson 1.5 discipline)

    // Bound widgets are valid here — safe to set initial values.
    if (HealthBar) { HealthBar->SetPercent(1.0f); }
    if (AmmoText)  { AmmoText->SetText(FText::FromString(TEXT("30 / 30"))); }
}
💡 NativeConstruct is where bindings are ready. Don't touch bound widgets in the C++ constructor — like an actor's constructor (Lesson 3.2), it runs too early, before the layout exists. NativeConstruct is the widget's "the layout is built, go" moment — the parallel to BeginPlay. The next lesson uses it to subscribe to gameplay events.

Hands-on Exercise & Quiz

🏋️ Exercise: A HUD scaffold

Objective: Set up the widget and show it.

  1. Create UPlayerHUD : public UUserWidget (Abstract) with BindWidget pointers for a UProgressBar* HealthBar and a UTextBlock* AmmoText.
  2. In the PlayerController, expose TSubclassOf<UPlayerHUD> HUDClass, and in BeginPlay create it and add it to the viewport.
  3. Set placeholder values in NativeConstruct.
  4. Why must the C++ pointer names match the Widget Blueprint widget names exactly?
✅ Answer to Q4

Because BindWidget links by name through the reflection system — Unreal finds the WBP widget whose name equals the C++ property name. A mismatch means it can't find the widget, so the Widget Blueprint fails to compile with a "missing bind widget" error. It's the same name-based reflection binding as AddDynamic in Module 5.

🎯 Quick Quiz

Question 1: What links a C++ pointer to a widget placed in a Widget Blueprint?

Question 2: Which puts a created widget on screen?

Question 3: Where can you first safely set values on bound widgets?

Summary

🎉 Key Takeaways

  • UMG splits like C++/Blueprint: Widget Blueprint = visual layout, C++ UUserWidget base = logic — the Lesson 6.4 pattern for UI.
  • Add the UMG module to .Build.cs; subclass UUserWidget (mark it Abstract).
  • UPROPERTY(meta = (BindWidget)) binds a C++ pointer to a same-named widget in the WBP (name match is mandatory); BindWidgetOptional for optional ones.
  • Show UI with CreateWidget<T>AddToViewport (from the PlayerController); store the instance as a UPROPERTY and RemoveFromParent when done.
  • NativeConstruct is the widget's BeginPlay — the first safe place to touch bound widgets.

📚 Additional Resources

🚀 What's Next?

The HUD is on screen with placeholder values. Now make it live: the next lesson drives it from gameplay — subscribing to the OnHealthChanged and OnAmmoChanged events you built in Module 5, and handling button clicks.

🎉 Lesson complete!

Your UI exists. Let's make it react to the game.