The prompt and the glyphs

The on-screen prompt, its why-not line, and input-device glyphs.

Assumes you have finished quick-start.

This page covers what the prompt shows, how to replace the widget, the glyph providers, the why-not line, the Client settings and the prompt's frame budget.

The kit draws a prompt for whatever the player is looking at, with the right button glyph for the device in their hands. You get it for free: install the plugin, add the two components, press Play. Nothing on this page is required reading to ship - it is here for when you want to change something.


What you see

Four things, and each one appears only when it has something to say.

What it isWhen it appears
The glyphThe button to press - an icon for a gamepad face button or a mouse button, a keycap with the key's name for everything elseWhenever the prompt is visible. It changes the moment the player switches device
The labelThe object's own prompt text (Open, Take, Read)Whenever the object has one. An empty label is legal - you get glyph only
The hold gaugeA bar filling while a hold is in progressOnly for a hold. An instant press never shows one, because a bar that appears and vanishes in one frame is noise
The "why not" lineOne line saying why the last attempt did not work - Out of range, Locked, Already interactingFor a moment after a refusal. A release is not a refusal, so letting go says nothing

The prompt shows for every object the player is looking at, usable or not. An object on cooldown is shown dimmed with its count-down; a busy object is shown and says so. Hiding it would leave the player wondering whether the game had noticed them at all.

The prompt is never focusable and never hit-testable. It cannot take input from your game or your menus, it is not part of gamepad navigation, and the kit re-asserts both on every update - so a widget that changes them is corrected rather than trusted.

The sample widget's palette is white on black, and it is deliberate rather than default: white label, white keycap letter, and a white rounded outline around the keycap, all on an 82 % black backing. Those pairings are what keep the prompt legible over a bright scene - the kit's own test measures them at >= 4.5:1 for text and >= 3:1 for the keycap frame, derived for both of the ways Slate may composite the backing's alpha. If you re-skin the widget (see below), that measurement goes with your colours: the kit asserts the contrast of the widget it ships, never of yours.


Replace the widget

You need one Blueprint and one event.

  1. Create a Widget Blueprint whose parent class is UK Prompt Widget Base.
  2. Override the event On Prompt State Changed. It hands you one Prompt State struct and it fires only when something in it changed - there is nothing to poll and nothing to tick.
  3. Point Project Settings > Plugins > UK Interaction > Client > Prompt Widget Class at it.

That is the whole contract. The kit creates one instance per local player, adds it to that player's screen at Prompt Z Order, keeps it non-interactive, destroys it with its PlayerController and builds a new one for the next.

An empty reparented Widget Blueprint does not inherit the kit's sample layout - the sample is built only into the kit's own shipped WBP_UKInteractionPrompt, so your widget draws exactly what you put in it (copy the shipped asset if you want the kit's layout as a starting point).

Which machine? On Prompt State Changed — Owning client only.

The thirteen fields

FUKPromptState is the only interface between the kit's logic and your UI. Every field is BlueprintReadOnly.

FieldTypeWhat it is
bVisibleboolWhether a prompt should be drawn at all. When it is false, every other field carries its pinned "hidden" value
TargetUK Interactable ComponentWhat the player is looking at. Check IsValid(Target) before you touch it - it can have been destroyed between the publish and your read (see below)
PromptTextTextThe object's resolved prompt text. Legally empty - draw glyph-only
UsabilityEUKUsabilityUsable / NotUsable / Cooldown / Disabled - what to dim, and whether to show a count-down
Priorityint32The focused object's priority, for a HUD that wants to style by importance
CooldownRemainingfloatSeconds left, quantised to 0.1 s so a HUD text does not churn every frame. 0 when there is no cooldown
GlyphFUKInputGlyphThe button to press: Key, bHasIcon, IconBrush, KeycapText, DeviceKind
bGaugeVisibleboolWhether a hold is in progress and a bar should be shown
HoldProgressfloat0..1, quantised to 1/256. A passthrough of the protocol's own progress, re-timed when the server's answer changes the hold duration
WhyNotTextTextThe refusal line's wording, already localised through the kit's reason table
WhyNotReasonEUKInteractionReasonThe same refusal as an enum, if you would rather choose your own wording
bWhyNotVisibleboolWhether that line is currently up
Serialint32Increments on every publish. Compare it for equality only - it wraps, and it keeps counting across a level change

Serial is int32, not uint32. Unreal Header Tool refuses a Blueprint-exposed uint32, and every field of this struct is BlueprintReadOnly (measured on 5.8: Type 'uint32' is not supported by blueprint). Nothing about the contract changes - it increments, it wraps, and it is compared by equality. [measured 5.8] [source-read 5.6] [source-read 5.7]

IsValid(Target) before you use it

The prompt publishes on change, and one of the changes it publishes is your target was destroyed. The kit hides the prompt on that frame, but a HUD that cached Target from an earlier publish and dereferences it later can be holding a dead pointer. Every graph that reads Target should run it through Is Valid first. This is the one lifetime rule the view-model cannot enforce for you.

Turning the kit's widget off

Two ways, and they mean different things:

  • Auto Create Prompt Widget = false - the kit never creates a widget at all. Publishing continues, so binding On Prompt State Changed on your own HUD still works. This is the setting for a project that draws its own prompt somewhere else entirely.
  • Set Prompt Widget Enabled(false) at runtime - the kit removes the widget it already made. Publishing keeps advancing while it is gone; Set Prompt Widget Enabled(true) builds a new one and it replays the last published state at once, so nothing has to be re-sent. Use this for a cutscene or a full-screen menu.

Clearing Prompt Widget Class works too, and is the bluntest form: nothing is created and a UK-VAL-070 warning is logged once if the path is set but does not load.

Which machine? On Prompt State Changed — Owning client only.


Glyphs

The glyph is resolved by a provider, and the kit ships one.

The default provider

UUKDefaultGlyphProvider asks Enhanced Input which keys are mapped to the interact action (Interact Action, or Prompt Input Action Fallback when that is unset), picks the one that matches the device the player last used, and answers with:

  • the icon from Glyph Set (DA_UKGlyphs ships 19 gamepad and mouse keys plus a keycap frame), or
  • a keycap carrying the key's own short display name when the set has no icon for it, or
  • a keycap reading "?" when nothing is mapped at all.

It resolves at most once per frame, only when something actually invalidated the glyph - a device change, a mapping rebuild, a settings change, a new controller - and never inside the delegate that triggered it. A burst of thirty rebuild events in one frame costs exactly one resolve.

Writing your own

Implement IUKGlyphProvider - one function, GetGlyph(Action, Kind, PC) returning a FUKInputGlyph. It is a BlueprintNativeEvent, so a Blueprint class can implement it too. Then install it, either way round:

  • Blueprint / per player: UK Prompt Subsystem > Set Glyph Provider.
  • C++ / process-wide: UK::Interaction::Glyphs::SetProvider(TScriptInterface<IUKGlyphProvider>), with UK::Interaction::Glyphs::InvalidateGlyphs() to force every live prompt to re-resolve.

Passing nullptr (or an empty interface) restores the kit's default. The slot is one per process, not one per player - a second SetProvider replaces the first, and every host is invalidated so the next tick shows the new glyph.

The resolution order is: an adapter registered under the kind Glyphs first, then your provider, then the default. That first slot is how Docs/adapters.md's CommonUI adapter will plug in without you changing anything - it returns the platform's own key brush and calls InvalidateGlyphs() when CommonUI's input type changes. The adapter is not in this version; the seam it registers into is.

Kind (the EUKInputDeviceKind the kit classified) is advisory. A provider with its own device authority - CommonUI's, for instance - is free to ignore it. What the kit publishes in Glyph.DeviceKind is always the kit's own classification, so a HUD reading that field gets a consistent answer regardless of which provider drew the icon.

Per-engine note (the rebuild delegate). UEnhancedInputLocalPlayerSubsystem::ControlMappingsRebuiltDelegate is a dynamic multicast (DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnControlMappingsRebuilt) - EnhancedInputSubsystems.h:52 on 5.6 / 5.7 / 5.8), and it never fires synchronously: AddMappingContext, RemoveMappingContext and RequestRebuildControlMappings() only raise bMappingRebuildPending, and even bForceImmediately merely rebuilds the mappings and sets bControlMappingsRebuiltThisTick. The broadcast happens on the next Enhanced Input module tick (FEnhancedInputModule::Tick -> HandleControlMappingRebuildDelegate()). So a context you add is reflected in the prompt's glyph on the following frame, not in the same call. [measured 5.8 - next module tick] [source-read 5.6 - next module tick] [source-read 5.7 - next module tick].

Per-engine note (key order). Enhanced Input's QueryKeysMappedToAction returns keys without their mapping-context priority, and the applied-context map is protected on every supported engine, so the default provider keeps the query order - which the engine already sorts by context priority (descending) when it rebuilds control mappings. [measured 5.8 - query order] [measured 5.6 - query order] [unmeasured - 5.7].

The glyph never resolves on the frame the interactor gathered - with one caveat you should know about. The prompt host defers a pending glyph resolve by one frame whenever the interactor's GatherCount advanced since the host's previous tick, so a 200 microsecond resolve and an 80 microsecond detection gather do not land on the same frame's budget. Because tickables run before the actor tick groups (FTickableGameObject::TickObjects sits between TG_PostPhysics and TG_PostUpdateWork on 5.6 / 5.7 / 5.8) the counter the host reads reports the previous frame's gather, so the deferral shifts the resolve to the frame after the one following a gather. At the default GatherIntervalSeconds (0.1 s - a gather every sixth frame at 60 Hz) neither frame gathers and the cost is one frame of latency on a device switch. At the knob's minimum (0.033 s - every second frame) the deferral can move the resolve onto the next gather frame instead; if you run detection that hot and profile a spike on a glyph change, that is why. The wait is bounded at one tick: once a frame time reaches GatherIntervalSeconds the interactor gathers on every frame, so an unbounded rule would never resolve the glyph at all - the host resolves on the second tick regardless of what the gather counter says.


The why-not line

When an interaction is refused, the prompt says so for a moment. When it merely ends, it says nothing - letting go of a hold is not a failure and does not deserve a line of text.

What arms it - four paths, all of them refusals:

PathExample
The server rejected the requestanother player got there first: Already interacting
The server cancelled a hold for a reason that is not a releaseyou walked away mid-hold: Out of range
The client's own prediction cancelled the holdthe same Moved reason, seen locally before the server's answer arrives
The client refused before sending anythingthe local pre-check: the object is on cooldown or unusable

That last row matters more than it looks. The kit checks usability before it sends an RPC, so most refusals a player sees never reach the server at all - the line is armed from the local denial, in the same call that refused the press. Nothing goes on the wire, and nothing has to round-trip before the player is told.

What does not arm it: a completed interaction, a release, and a Rejected carrying the reason None. The last one is deliberate - None is the reason a completion carries, so showing a line for it would put the word "None" on screen. The kit logs one Verbose line instead so the silence is visible in a log.

The rules, all four of them:

  • Replace, never queue. A second refusal overwrites the first and restarts the window from its own time. There is no backlog of complaints.
  • One window, from settings. The line hides at armed + Why Not Seconds (default 1.5, range 0.2-10.0), on the first frame at or after that time - at every frame rate. Changing the setting while a line is up applies to that line.
  • Disarmed by a focus change, and by a later accept. Look at something else and the line goes with your attention; press again and succeed and it goes too. A usability change on the same object does not disarm it - the object saying "no" twice is one complaint, not two.
  • Dropped when it is about something you stopped looking at. A rejection that arrives after the player has moved on is counted (FUKPromptStats) and logged at Verbose, and nothing is shown. This is the only place where a real server answer is deliberately discarded.

The wording comes from one place: Get Reason Text (Docs/events.md > Reasons), which reads the kit's shipped string table and so is localisable. WhyNotReason carries the same refusal as an enum if you would rather write your own line. Local denials are mapped into that same closed enum before the lookup, so there is exactly one vocabulary: NotUsable -> NotUsable, AlreadyInteracting -> AlreadyInteracting, and nothing focused -> no line at all.


Settings

Project Settings > Plugins > UK Interaction, section Client. Eleven fields.

The four asset fields are soft paths, never hard references: you can delete or replace any of the shipped assets and nothing in the kit's C++ stops compiling.

SettingTypeRangeDefaultWhat it does
Prompt Widget ClassFSoftClassPath-WBP_UKInteractionPrompt.WBP_UKInteractionPrompt_CThe widget the kit creates for each local player. Point it at your own UKPromptWidgetBase subclass, or clear it and draw the prompt yourself.
Auto Create Prompt Widgetbool-trueWhether the kit creates that widget at all. Turn it off and nothing is drawn until you bind On Prompt State Changed.
Glyph SetFSoftObjectPath-DA_UKGlyphsWhere the glyph icons come from. Empty is legal - every key then renders as a keycap with its own name.
Prompt Input Action FallbackFSoftObjectPath-IA_UK_InteractThe action the glyph layer asks about when Interact Action is unset.
Why Not Secondsfloat0.2 - 10.01.5How long a refusal line stays on screen. A newer refusal replaces it and restarts the window; refusals are never queued.
Prompt Z Orderint32-100 - 100010Where the prompt sits in the viewport. Below your HUD's Z order, your HUD covers it.
Dimmed Opacityfloat0.0 - 1.00.5How far the glyph dims while the focused object cannot be used right now.
Interact ActionFSoftObjectPath-IA_UK_Interact.IA_UK_InteractThe interact action the kit binds - and the one the prompt shows a glyph for.
Mapping ContextFSoftObjectPath-IMC_UK_Default.IMC_UK_DefaultThe mapping context the kit adds for that action.
Mapping Context Priorityint32any int320The priority it is added at. Deliberately unclamped.
Add Mapping Contextbool-trueWhether the kit adds the context at all. Turn it off if you own the mapping.

The last four are the input knobs. They belong to the kit's Enhanced Input binding rather than to the prompt, and they sit in this same section because that is where a project looks for them. The prompt reads exactly one of them - Interact Action, to know which action's glyph to show - and falls back to Prompt Input Action Fallback when it is unset.

Mapping Context Priority is deliberately unclamped. Whether it should have a range is an open decision, recorded rather than guessed.

Which machine? On Prompt State Changed — Owning client only.


Budgets

The prompt layer has five costs, and a ceiling for each. The kit's own measurements are far under them, which is the honest way to say that this is not where your frame time goes.

TermWhat it isTargetCeilingMeasured
c_publishRecompute + broadcast, host only (no widget)10 µs20 µs0.5 µs
c_widget_fullThe shipped widget's handler when text or visibility changed30 µs60 µs1.1 µs
c_widget_gaugeThe same handler when only the gauge moved8 µs15 µs0.5 µs
c_tickOne host tick while something is live5 µs10 µs0.6 µs
c_glyphOne glyph resolve, including the icon lookup100 µs200 µs0.8 µs

An idle prompt costs zero: the host is an FTickableGameObject whose IsTickable() is false unless a gauge is running, a cooldown is counting down or a glyph is dirty. 600 idle frames move neither a publish nor a resolve, and the tick is not registered at all.

The shared client budget is 200 µs per frame, and the prompt is one of three claimants. The other two are the interactor's detection gather and the interaction protocol's predicted tick. On the kit's own test machine, a hold frame measures 1.5 µs for the prompt and the interactor's gather 7.1 µs - 8.6 µs of 200. Adding the protocol's measured predicted tick gives 9.3 µs; adding its budgeted 10 µs claim instead gives 18.6 µs. Both are inside the budget, so the prompt layer accepts the protocol's claim rather than contesting it.

Three cases exceed 200 µs at ceilings, by design, and are documented rather than gated:

CaseSumWhy it is recorded and not fixed
A detection gather plus a glyph resolve at ceiling280.4 µsThis is the reason for the gather-frame deferral - the two never share a frame
The detection knob corner plus a focus-change frame231.6 µsOnly reachable by turning detection's knobs to their most expensive corner
Split-screen: two players each gathering and holding250.8 µsTwo local players are two of everything, including two budgets' worth of work

These are ceiling arithmetic, not measurements. Nothing in the kit has been observed near them.

What the exit fade costs: 0.3 µs per frame was measured on the "cannot play" branch, before the shipped widget's fade played; the playing fade has not been re-measured (see § Known gaps). It is recorded, never gated, and it adds no tick reason: the fade lives on the widget, not on the host.

All five figures are advisory, measured on one machine (a developer box, not a dedicated benchmark rig) on 5.8 only, on 2026-09-09. They are upper bounds: the measurement overhead is never subtracted, and the numbers come from an uncooked editor build, so they may not be quoted as shipping costs.


Known gaps

Everything on this list is a thing the kit does not prove, stated so you do not have to discover it.

  • "The widget is really on screen" is not asserted anywhere. No test in this kit owns a real viewport, so the kit claims only the half it can see: the widget is created, its Add To Player Screen did not reach a screen, and exactly one UK-VAL-073 warning says so. Whether a headless test could own one was measured on 5.8: a UGameViewportClient can be hand-built and the player-screen slot does record Prompt Z Order - but a viewport client that never ran in a real window has no GameLayerManager, so the canvas dies as the call returns. Add To Player Screen also returns true in that situation, which is why the kit reads IsInViewport() for the answer instead of trusting the return value. [measured 5.8 - no] [unmeasured - 5.6] [unmeasured - 5.7]

  • Engine coverage. In one line: "supported on 5.6–5.8; multiplayer end-to-end suite run on all three". On 5.6, 5.7 and 5.8 the compile, logic, reflection and structural suites pass, and so do the session tests.

  • OnPlayerControllerChanged firing points: measured only NEGATIVELY. In the kit's in-process fixtures the delegate never fires - the player is set before the local player is added - so the host's controller half runs only through its Initialize-time evaluation, which is exactly why that evaluation exists. The kit's level-travel test re-creates the controller and the delegate does fire there, so the recreate path is observed; the construction-time firing points are not. A shipping game reaches the delegate normally. [measured 5.8 - fixture-scoped] [unmeasured - 5.6] [unmeasured - 5.7]

  • Tickables run BEFORE the actor tick groups, not after (measured). FTickableGameObject::TickObjects sits between TG_PostPhysics and TG_PostUpdateWork (5.6 LevelTick.cpp:1582 / 5.7 :1792 / 5.8 :1821), and the interactor pins its detection tick to TG_PostUpdateWork - so the prompt host ticks first and the gather counter it reads reports the previous frame's gather. The gather-frame deferral is implemented against that measurement and its consequence is written out under § Glyphs. [measured 5.8] [source-read 5.6] [source-read 5.7]

  • Focus usability lags a replicated change by up to one detection gather interval. The interactor caches an interactable's usability when it gathers (every 0.1 s at the default), so for up to that long a prompt can read Usable for an object another player has just taken - and the other way round. It is design, not a defect: it is what keeps detection off the per-frame path. It is also why a refusal often arms from the client's own pre-check rather than from a server verdict.

  • Changing a setting at runtime from C++ does not re-publish. The prompt re-reads Why Not Seconds and the other Client knobs when something else drives a recompute, and in the editor it subscribes to the settings-changed delegate so a Details-panel edit applies at once. A packaged build writing the CDO through GetMutableDefault<UUKInteractionSettings>() is silent until the next publish - there is no runtime broadcaster for that path in the engine.

  • No reduced-motion setting exists to honour (measured). None of 5.6 / 5.7 / 5.8 exposes one the kit could read - FSlateApplication::AreMenuAnimationsEnabled() is an editor menu preference and there is no UAccessibilitySettings class - so the kit adds no bReduceMotion knob of its own rather than inventing a second place to configure the same idea. If your project has an accessibility menu, turn the fade off by shipping your own widget.

  • The prompt's visual design has not had its formal UX review. The layout, the 0.82 backing opacity and the 120 ms exit fade come from the kit's prompt design spec, which is still in design. Those numbers may still move; the code that reads them will not.

  • The exit fade's per-frame cost has not been re-measured since the sample widget began playing it. The shipped prompt fades out over 120 ms. Your own widget can carry an animation named FadeOut (the name in its Animations panel) and the kit will play it on exit and restore whatever it animated when it stops. The animation must live on your Widget Blueprint itself - one inherited from a parent Widget Blueprint is not looked up, and that widget exits instantly.

  • The world-anchored prompt (drawn at the object rather than at the bottom of the screen) is not in this version. Neither is a CommonUI glyph adapter. Both have their seams here already.