Skip to main content

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 ClassGameplay PatternDriven By
UERPInteractionWidgetBaseSimple prompt ("Press E to Open")FERPInteractionDescriptor
UERPHoldInteractionWidgetBaseHold-to-interact, multi-pressFERPInteractionDescriptor + progress
UERPTargetingWidgetBaseReticle, lock-on, target infoUERPTargetDescriptor

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:

FieldTypePurposeWidget Usage
DisplayNameFTextInteraction labelTextBlock ("Open", "Talk", "Loot")
PromptTextFTextFull promptTextBlock ("Press E to open")
InputActionUInputAction*Key binding referenceGetCurrentInputDisplayKey() returns "E", "F", etc.
IconTSoftObjectPtr<UTexture2D>Interaction iconImage widget (load soft ref)
HoldDurationfloat0 = instant, >0 = hold (seconds)Determines widget type (prompt vs progress bar)
RequiredPressesint320 = not multi-press, >0 = press N timesDiscrete progress steps
InteractionTagsFGameplayTagContainerGameplay tagsConditional styling
PresentationHintsTMap<FName, UObject*>Custom dataGame-specific extensions

Helper methods:

  • IsHoldInteraction() — returns true if HoldDuration > 0
  • IsMultiPressInteraction() — returns true if RequiredPresses > 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

  1. Content Browser -> Widget Blueprint
  2. Reparent to ERPInteractionWidgetBase (Class Settings -> Parent Class)
  3. 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

  1. Widget Blueprint -> Reparent to ERPHoldInteractionWidgetBase
  2. 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

  1. Widget Blueprint -> Reparent to ERPHoldInteractionWidgetBase
  2. 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

  1. Widget Blueprint -> Reparent to ERPTargetingWidgetBase
  2. 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

  1. Widget Blueprint -> Reparent to ERPTargetingWidgetBase
  2. 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 LocationWhen to Use
PlayerControllerMost common. Owns the interaction/targeting components.
HUD BlueprintWhen your HUD already manages all UI. Access components via GetOwningPlayerController.
Custom Actor ComponentWhen you want reusable presenter logic across multiple characters.
Widget itselfOnly 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:

  1. Create a Widget Blueprint (no special parent class)
  2. Add a custom event or function: UpdateFromData(FERPInteractionDescriptor Descriptor)
  3. 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

PatternWidget BaseDescriptor FieldPresenter Drives
Simple PromptERPInteractionWidgetBase(all standard fields)UpdateFromDescriptor + SetFocused
Hold-to-InteractERPHoldInteractionWidgetBaseHoldDuration > 0SetHoldProgress(time / duration) per frame
Multi-PressERPHoldInteractionWidgetBaseRequiredPresses > 0SetHoldProgress(presses / required) per press
Target ReticleERPTargetingWidgetBase(from UERPTargetDescriptor)SetTargetActive + SetTargetActor
Target InfoERPTargetingWidgetBase(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.