Widget Guide
Complete guide to building interaction and targeting UI using the plugin's widget base classes.
Overview
The plugin provides three abstract widget base classes that you extend in Blueprint to create your game's UI:
| Base Class | Gameplay Pattern | Driven By |
|---|---|---|
UERPInteractionWidgetBase | Simple prompt ("Press E to Open") | FERPInteractionDescriptor |
UERPHoldInteractionWidgetBase | Hold-to-interact, multi-press | FERPInteractionDescriptor + progress |
UERPTargetingWidgetBase | Reticle, lock-on, target info | UERPTargetDescriptor |
Key principles:
- Widgets are passive — they receive data, they don't fetch it
- Widgets do not manage their own lifecycle — a presenter creates, shows, hides, and destroys them
- All widget data comes from descriptors — the data contract between game logic and UI
- All hooks are
BlueprintNativeEvent— override in Blueprint or C++
Architecture
[Perception] → [Domain Component] → [Presenter] → [Widget]
detects validates wires displays
candidates eligibility events UI
tracks state manages
fires events lifecycle
Perception selects the best candidate per channel.
Domain (InteractionComponent, TargetingComponent) validates the candidate, pulls descriptors, fires events.
Presenter (your code, typically on PlayerController or HUD) listens to domain events and drives widgets.
Widget (your WBP extending a base class) receives data and displays it.
The plugin provides everything except the presenter — that's intentionally game-specific. This guide shows you exactly how to build one for each pattern.
The Interaction Descriptor
Every interaction widget is driven by FERPInteractionDescriptor. Understanding its fields is essential:
| Field | Type | Purpose | Widget Usage |
|---|---|---|---|
DisplayName | FText | Interaction label | TextBlock ("Open", "Talk", "Loot") |
PromptText | FText | Full prompt | TextBlock ("Press E to open") |
InputAction | UInputAction* | Key binding reference | GetCurrentInputDisplayKey() returns "E", "F", etc. |
Icon | TSoftObjectPtr<UTexture2D> | Interaction icon | Image widget (load soft ref) |
HoldDuration | float | 0 = instant, >0 = hold (seconds) | Determines widget type (prompt vs progress bar) |
RequiredPresses | int32 | 0 = not multi-press, >0 = press N times | Discrete progress steps |
InteractionTags | FGameplayTagContainer | Gameplay tags | Conditional styling |
PresentationHints | TMap<FName, UObject*> | Custom data | Game-specific extensions |
Helper methods:
IsHoldInteraction()— returns true ifHoldDuration > 0IsMultiPressInteraction()— returns true ifRequiredPresses > 0
The interactable actor fills this descriptor in GetInteractionDescriptor. The widget reads it.
Pattern 1: Simple Prompt
Use case: RPG, adventure, puzzle games — "Press E to Open"
Base class: UERPInteractionWidgetBase
Step 1: Create the Widget Blueprint
- Content Browser -> Widget Blueprint
- Reparent to
ERPInteractionWidgetBase(Class Settings -> Parent Class) - Name:
WBP_SimplePrompt
Step 2: Design the Layout
Build your UMG layout. Typical elements:
- TextBlock
PromptText— displays the interaction prompt - TextBlock
KeyText— displays the key binding ("E") - Image
IconImage— displays the interaction icon
Example layout:
[HorizontalBox]
[Image: IconImage]
[VerticalBox]
[TextBlock: PromptText] // "Open Door"
[TextBlock: KeyText] // "[E]"
Step 3: Override OnDescriptorUpdated
This is called whenever the interaction descriptor changes (new candidate, or same candidate with updated state).
Event: On Descriptor Updated (Descriptor)
// Update prompt text
-> Set Text (PromptText): Descriptor.DisplayName
// Update key binding display
-> Set Text (KeyText): GetCurrentInputDisplayKey()
// Update icon (load soft reference)
-> Branch: Is Valid (Descriptor.Icon)
True:
-> Async Load Asset (Descriptor.Icon)
-> Set Brush From Texture (IconImage, Loaded Texture)
-> Set Visibility (IconImage): Visible
False:
-> Set Visibility (IconImage): Collapsed
Step 4: Override OnFocusChanged (optional)
Default behavior toggles visibility. Override for animations:
Event: On Focus Changed (bNewFocused)
-> Branch: bNewFocused
True: -> Play Animation (FadeIn)
False: -> Play Animation (FadeOut)
Step 5: Build the Presenter
On your PlayerController (or HUD Blueprint):
Variables:
InteractionWidget(type:WBP_SimplePrompt, reference)
Construction:
Event BeginPlay
// Create widget
-> Create Widget (WBP_SimplePrompt, Owning Player: Self)
-> Set InteractionWidget
-> Add to Viewport
// Bind to interaction events
-> Get Component (ERPInteractionComponent)
-> Bind OnDescriptorChanged
-> Bind OnDomainCandidateChanged
Event wiring:
Event: OnDescriptorChanged (Candidate, Descriptor, ChannelId)
-> InteractionWidget -> UpdateFromDescriptor(Descriptor)
-> InteractionWidget -> SetFocused(true)
Event: OnDomainCandidateChanged (Previous, New, ChannelId)
-> Branch: NOT IsValid(New)
True: -> InteractionWidget -> SetFocused(false)
That's it. When the player walks near an interactable, the widget appears with the correct prompt. When they walk away, it hides.
Pattern 2: Hold-to-Interact
Use case: Survival, looter, horror — "Hold E to Loot" with a progress bar
Base class: UERPHoldInteractionWidgetBase
Step 1: Create the Widget Blueprint
- Widget Blueprint -> Reparent to
ERPHoldInteractionWidgetBase - Name:
WBP_HoldPrompt
Step 2: Design the Layout
[VerticalBox]
[TextBlock: PromptText] // "Hold to Loot"
[TextBlock: KeyText] // "[E]"
[ProgressBar: HoldProgress] // Fills as player holds
Step 3: Override Hooks
OnDescriptorUpdated — same as simple prompt:
Event: On Descriptor Updated (Descriptor)
-> Set Text (PromptText): Descriptor.DisplayName
-> Set Text (KeyText): GetCurrentInputDisplayKey()
OnHoldProgressChanged — update the progress bar:
Event: On Hold Progress Changed (Progress)
-> Set Percent (HoldProgress): Progress
OnHoldStarted (optional):
Event: On Hold Started
-> Set Color (HoldProgress): Yellow
-> Play Sound: HoldStartSFX
OnHoldCompleted (optional):
Event: On Hold Completed
-> Set Color (HoldProgress): Green
-> Play Sound: CompleteSFX
OnHoldCancelled (optional):
Event: On Hold Cancelled
-> Set Percent (HoldProgress): 0.0
-> Set Color (HoldProgress): White
Step 4: Build the Presenter
The presenter manages the hold timer. The widget only displays progress.
Variables:
HoldWidget(type:WBP_HoldPrompt)bIsHolding(bool)HoldAccumulator(float)CurrentHoldDuration(float)CurrentDescriptor(FERPInteractionDescriptor)
Bind events (same as simple prompt, but also store the descriptor):
Event: OnDescriptorChanged (Candidate, Descriptor, ChannelId)
-> Set CurrentDescriptor = Descriptor
-> Set CurrentHoldDuration = Descriptor.HoldDuration
-> HoldWidget -> UpdateFromDescriptor(Descriptor)
-> HoldWidget -> SetFocused(true)
Event: OnDomainCandidateChanged (Previous, New, ChannelId)
-> Branch: NOT IsValid(New)
True:
-> HoldWidget -> ResetHold()
-> HoldWidget -> SetFocused(false)
-> Set bIsHolding = false
-> Set HoldAccumulator = 0.0
Handle input (Enhanced Input — Started, Triggered, Completed):
Input Action: Interact (Started)
-> Branch: CurrentHoldDuration > 0
True:
-> Set bIsHolding = true
-> Set HoldAccumulator = 0.0
False:
-> InteractionComponent -> RequestInteraction() // Instant
Input Action: Interact (Completed / Cancelled)
-> Set bIsHolding = false
-> Set HoldAccumulator = 0.0
-> HoldWidget -> ResetHold()
Tick (drives the progress):
Event Tick (DeltaTime)
-> Branch: bIsHolding AND CurrentHoldDuration > 0
True:
-> HoldAccumulator += DeltaTime
-> Progress = HoldAccumulator / CurrentHoldDuration
-> HoldWidget -> SetHoldProgress(Progress)
-> Branch: Progress >= 1.0
True:
-> InteractionComponent -> RequestInteraction()
-> Set bIsHolding = false
The widget handles the visual state automatically — OnHoldStarted fires on first progress, OnHoldProgressChanged fires each frame, OnHoldCompleted fires at 1.0.
Pattern 3: Multi-Press
Use case: Action games — "Mash E to Break Free" (press 5 times)
Base class: UERPHoldInteractionWidgetBase (same as hold — progress goes in discrete steps)
Step 1: Create the Widget Blueprint
- Widget Blueprint -> Reparent to
ERPHoldInteractionWidgetBase - Name:
WBP_MultiPressPrompt
Step 2: Design the Layout
[VerticalBox]
[TextBlock: PromptText] // "Break Free!"
[TextBlock: CounterText] // "3 / 5"
[ProgressBar: PressProgress] // Fills in steps
Step 3: Override Hooks
OnHoldProgressChanged — update progress bar AND counter text:
Event: On Hold Progress Changed (Progress)
-> Set Percent (PressProgress): Progress
// Calculate press count from progress
-> RequiredPresses = GetDescriptor().RequiredPresses
-> CurrentPresses = Round(Progress * RequiredPresses)
-> Set Text (CounterText): Format("{0} / {1}", CurrentPresses, RequiredPresses)
OnHoldCompleted:
Event: On Hold Completed
-> Play Animation: BreakFreeAnimation
-> Play Sound: BreakFreeSFX
Step 4: Build the Presenter
Variables:
MultiPressWidget(type:WBP_MultiPressPrompt)CurrentPresses(int32)RequiredPresses(int32)
Bind events:
Event: OnDescriptorChanged (Candidate, Descriptor, ChannelId)
-> Set RequiredPresses = Descriptor.RequiredPresses
-> Set CurrentPresses = 0
-> MultiPressWidget -> UpdateFromDescriptor(Descriptor)
-> MultiPressWidget -> SetFocused(true)
Event: OnDomainCandidateChanged (Previous, New, ChannelId)
-> Branch: NOT IsValid(New)
True:
-> MultiPressWidget -> ResetHold()
-> MultiPressWidget -> SetFocused(false)
-> Set CurrentPresses = 0
Handle input (each press increments):
Input Action: Interact (Started)
-> Branch: RequiredPresses > 0
True:
-> CurrentPresses++
-> Progress = (float)CurrentPresses / (float)RequiredPresses
-> MultiPressWidget -> SetHoldProgress(Progress)
-> Branch: CurrentPresses >= RequiredPresses
True:
-> InteractionComponent -> RequestInteraction()
-> Set CurrentPresses = 0
False:
-> InteractionComponent -> RequestInteraction() // Instant
The hold widget fires OnHoldStarted on first press, OnHoldProgressChanged on every press, and OnHoldCompleted when the count is reached.
Pattern 4: Target Reticle
Use case: Action, shooter — lock-on marker on the target
Base class: UERPTargetingWidgetBase
Step 1: Create the Widget Blueprint
- Widget Blueprint -> Reparent to
ERPTargetingWidgetBase - Name:
WBP_TargetReticle
Step 2: Design the Layout
[CanvasPanel]
[Image: ReticleImage] // Centered crosshair / bracket / diamond
Step 3: Override Hooks
OnTargetActiveChanged:
Event: On Target Active Changed (bActive)
-> Branch: bActive
True: -> Play Animation (LockOnAnimation)
False: -> Play Animation (LockOffAnimation)
OnDescriptorUpdated (optional, for descriptor-driven styling):
Event: On Descriptor Updated (Descriptor)
-> Branch: IsValid(Descriptor)
True:
// Color reticle based on target tags
-> Branch: Descriptor.TargetTags HasTag "Hostile"
True: -> Set Color (ReticleImage): Red
False: -> Set Color (ReticleImage): White
Step 4: Build the Presenter
For a target reticle, the presenter also needs to position the widget over the target in screen space.
Variables:
ReticleWidget(type:WBP_TargetReticle)
Construction and binding:
Event BeginPlay
-> Create Widget (WBP_TargetReticle)
-> Add to Viewport
-> ReticleWidget -> SetFocused(false) // Hidden initially
-> TargetingComponent -> Bind OnDomainCandidateChanged
-> TargetingComponent -> Bind OnDescriptorChanged
Event wiring:
Event: OnDomainCandidateChanged (Previous, New, ChannelId)
-> ReticleWidget -> SetTargetActor(New)
-> ReticleWidget -> SetTargetActive(IsValid(New))
Event: OnDescriptorChanged (Candidate, Descriptor, ChannelId)
-> ReticleWidget -> UpdateFromDescriptor(Descriptor)
Tick (position reticle on screen):
Event Tick
-> TargetActor = ReticleWidget -> GetTargetActor()
-> Branch: IsValid(TargetActor) AND ReticleWidget -> IsTargetActive()
True:
-> Project World Location to Screen (TargetActor.GetActorLocation())
-> Set Position in Viewport (ReticleWidget, ScreenPosition)
Pattern 5: Target Info Panel
Use case: RPG, ARPG — floating nameplate with name, icon, health
Base class: UERPTargetingWidgetBase
Step 1: Create the Widget Blueprint
- Widget Blueprint -> Reparent to
ERPTargetingWidgetBase - Name:
WBP_TargetInfoPanel
Step 2: Design the Layout
[VerticalBox]
[HorizontalBox]
[Image: TargetIcon]
[TextBlock: TargetName] // "Goblin Warrior"
[ProgressBar: HealthBar] // Populated from actor data
[TextBlock: DistanceText] // "15m"
Step 3: Override Hooks
OnDescriptorUpdated:
Event: On Descriptor Updated (Descriptor)
-> Branch: IsValid(Descriptor)
True:
-> Set Text (TargetName): Descriptor.DisplayName
-> Branch: Is Valid (Descriptor.Icon)
True: -> Async Load Asset -> Set Brush (TargetIcon)
// Style based on tags
-> Branch: Descriptor.TargetTags HasTag "Boss"
True: -> Set Color (TargetName): Gold
False:
-> Set Text (TargetName): "Unknown"
OnTargetActorChanged — query actor-specific data:
Event: On Target Actor Changed (NewTargetActor)
-> Branch: IsValid(NewTargetActor)
True:
// Get health from your game's health component
-> Get Component (HealthComponent) from NewTargetActor
-> Branch: IsValid(HealthComponent)
True: -> Set Percent (HealthBar): HealthComponent.GetHealthPercent()
-> Set Visibility (HealthBar): Visible
False: -> Set Visibility (HealthBar): Collapsed
Step 4: Build the Presenter
Same wiring as the target reticle pattern:
Event: OnDomainCandidateChanged (Previous, New, ChannelId)
-> InfoPanel -> SetTargetActor(New)
-> InfoPanel -> SetTargetActive(IsValid(New))
Event: OnDescriptorChanged (Candidate, Descriptor, ChannelId)
-> InfoPanel -> UpdateFromDescriptor(Descriptor)
For the health bar to update in real-time, either tick-poll or bind to the health component's events in OnTargetActorChanged.
Choosing the Right Widget Type
Use the descriptor to decide which widget to show:
Event: OnDescriptorChanged (Candidate, Descriptor, ChannelId)
-> Branch: Descriptor.IsMultiPressInteraction()
True: -> Show MultiPressWidget
-> Branch: Descriptor.IsHoldInteraction()
True: -> Show HoldWidget
-> Otherwise:
-> Show SimplePromptWidget
For games with mixed interaction types (some doors are instant, some chests require holding, some traps require mashing), you can create all three widgets at startup and swap between them based on the descriptor.
Choosing the Right Presenter Location
| Presenter Location | When to Use |
|---|---|
| PlayerController | Most common. Owns the interaction/targeting components. |
| HUD Blueprint | When your HUD already manages all UI. Access components via GetOwningPlayerController. |
| Custom Actor Component | When you want reusable presenter logic across multiple characters. |
| Widget itself | Only for very simple cases. Generally avoid — keeps widgets passive. |
Building Custom Widgets from Scratch
If the base classes don't fit your needs, you can build directly on UUserWidget:
- Create a Widget Blueprint (no special parent class)
- Add a custom event or function:
UpdateFromData(FERPInteractionDescriptor Descriptor) - Wire from presenter exactly the same way
The base classes provide convenience (cached descriptor, focus tracking, input key helper), but you're not required to use them. The architecture is the same either way: descriptor flows from domain to presenter to widget.
Using PresentationHints
For game-specific data that doesn't fit standard descriptor fields, use PresentationHints:
On the interactable actor:
Event: Get Interaction Descriptor
-> Make Descriptor
-> Add to PresentationHints Map:
Key: "CurrencyCost"
Value: Your UObject wrapping the cost data
-> Set Out Descriptor
On the widget:
Event: On Descriptor Updated (Descriptor)
-> Find in Map (Descriptor.PresentationHints, "CurrencyCost")
-> Branch: Found
True: -> Cast to UCostData -> Set Text (CostText): Cost.Amount
C++ Presenter Example
For C++ projects, here's a minimal presenter component:
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Domains/Interaction/ERPInteractionDescriptor.h"
#include "ERPInteractionPresenter.generated.h"
class UERPInteractionComponent;
class UERPInteractionWidgetBase;
UCLASS(Blueprintable, meta=(BlueprintSpawnableComponent))
class YOURGAME_API UERPInteractionPresenter : public UActorComponent
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Presenter")
TSubclassOf<UERPInteractionWidgetBase> WidgetClass;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
UPROPERTY(Transient)
TObjectPtr<UERPInteractionWidgetBase> Widget;
UPROPERTY(Transient)
TObjectPtr<UERPInteractionComponent> InteractionComponent;
UFUNCTION()
void OnDescriptorChanged(AActor* Candidate,
const FERPInteractionDescriptor& Descriptor, FName ChannelId);
UFUNCTION()
void OnCandidateChanged(AActor* Previous, AActor* New, FName ChannelId);
};
#include "ERPInteractionPresenter.h"
#include "Domains/Interaction/ERPInteractionComponent.h"
#include "UI/Interaction/ERPInteractionWidgetBase.h"
#include "GameFramework/PlayerController.h"
void UERPInteractionPresenter::BeginPlay()
{
Super::BeginPlay();
InteractionComponent = GetOwner()->FindComponentByClass<UERPInteractionComponent>();
if (!InteractionComponent || !WidgetClass)
{
return;
}
APlayerController* PC = Cast<APlayerController>(GetOwner());
if (!PC)
{
return;
}
Widget = CreateWidget<UERPInteractionWidgetBase>(PC, WidgetClass);
if (Widget)
{
Widget->AddToViewport();
Widget->SetFocused(false);
}
InteractionComponent->OnDescriptorChanged.AddDynamic(
this, &UERPInteractionPresenter::OnDescriptorChanged);
InteractionComponent->OnDomainCandidateChanged.AddDynamic(
this, &UERPInteractionPresenter::OnCandidateChanged);
}
void UERPInteractionPresenter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (Widget)
{
Widget->RemoveFromParent();
Widget = nullptr;
}
Super::EndPlay(EndPlayReason);
}
void UERPInteractionPresenter::OnDescriptorChanged(AActor* Candidate,
const FERPInteractionDescriptor& Descriptor, FName ChannelId)
{
if (Widget)
{
Widget->UpdateFromDescriptor(Descriptor);
Widget->SetFocused(true);
}
}
void UERPInteractionPresenter::OnCandidateChanged(AActor* Previous,
AActor* New, FName ChannelId)
{
if (Widget && !New)
{
Widget->SetFocused(false);
}
}
This presenter component auto-wires everything. Add it to your PlayerController, set WidgetClass to your WBP, and it works.
Summary
| Pattern | Widget Base | Descriptor Field | Presenter Drives |
|---|---|---|---|
| Simple Prompt | ERPInteractionWidgetBase | (all standard fields) | UpdateFromDescriptor + SetFocused |
| Hold-to-Interact | ERPHoldInteractionWidgetBase | HoldDuration > 0 | SetHoldProgress(time / duration) per frame |
| Multi-Press | ERPHoldInteractionWidgetBase | RequiredPresses > 0 | SetHoldProgress(presses / required) per press |
| Target Reticle | ERPTargetingWidgetBase | (from UERPTargetDescriptor) | SetTargetActive + SetTargetActor |
| Target Info | ERPTargetingWidgetBase | (from UERPTargetDescriptor) | UpdateFromDescriptor + SetTargetActor |
Next Steps
Set up interactions with Interaction Guide.
Set up targeting with Targeting Guide.
Create custom domains with Custom Domains.
Check complete API in API Reference.