Detection — the profile, the gather sources, and line of sight
Detection profiles, gather sources and line of sight.
Assumes you have finished quick-start.
This page covers the detection profile, the two gather sources, line of sight, and the validator rules
UK-VAL-030–UK-VAL-035.
The UK Interactor component runs on the locally controlled player pawn only. Every detection tick it:
- gathers candidate interactables on a time cadence (default every
0.1 s), - filters and scores the pool every frame into exactly one focus, and
- confirms the leader with one line-of-sight trace from the pawn's eyes.
It never sends an RPC, never replicates focus, and never runs on a dedicated server, a simulated proxy or an AI pawn.
The detection profile
UUKDetectionProfile is a UDataAsset with 16 fields. Resolution order, and it never ends in
"no detection":
the component's own profile → UUKInteractionSettings::DefaultDetectionProfile → class defaults
Falling through to class defaults logs UK-VAL-031 once (a Warning, not a failure). A field
outside its range is re-clamped at resolve with one Warning per field — an out-of-range asset always
detects, it just detects at the clamped value.
| Field | Default | Range | What it does |
|---|---|---|---|
Strategy | Hybrid | — | Hybrid = periodic gather + per-frame scoring + LOS on the leader. Aim (Advanced) = one sweep per tick, and the sweep is the LOS. |
GatherSource | Registry | — | Registry (default) or PhysicsOverlap (Advanced). See below. |
GatherIntervalSeconds | 0.1 | 0.033 – 0.5 | How often the candidate pool is rebuilt. |
GatherRadiusCm | 1000.0 | 100 – 10000 | Pool radius around the pawn. |
ViewConeHalfAngleDeg | 60.0 | 10 – 180 | Half-angle of the gather cone. 180 disables the cone. |
MaxCandidates | 16 | 4 – 64 | Pool cap, applied after the cone. |
LosTraceChannel | Visibility | — | Trace channel for the line-of-sight check. |
GatherObjectTypes | WorldStatic, WorldDynamic, PhysicsBody, Pawn | — | PhysicsOverlap source only. |
bTraceComplex | false | — | Simple vs complex collision for the LOS trace. See "the complex-only wall". |
AimSweepRadiusCm | 20.0 | 0 – 100 | Aim strategy sweep radius. |
DetectionOrigin | ControllerView | — | Where the aim direction comes from. The LOS origin is always the eyes. |
HysteresisAngleDeg | 5.0 | 0 – 30 | A challenger must beat the current focus by this much on angle. |
HysteresisDistanceCm | 25.0 | 0 – 200 | …and by this much on distance. |
FocusDwellSeconds | 0.1 | 0 – 0.5 | Minimum time a focus is held before hysteresis is even consulted. |
LosGraceSeconds | 0.15 | 0 – 0.5 | A freshly blocked focus is kept this long before it is dropped. |
bFocusNonUsable | true | — | Whether a non-usable interactable can hold focus (so the prompt can say why not). |
Focus order is fixed and is not a weighted sum: usable first, then designer Priority, then aim
angle, then distance, then a stable id.
Registry vs PhysicsOverlap — and why Registry is the default
Registry (default)
UUKInteractionWorldSubsystem is a core UWorldSubsystem holding every live interactable: every
UK Interactable registers in its BeginPlay and unregisters in its EndPlay, on every machine.
The gather walks that set once per gather interval — radius² test, then cone test, then cap at
MaxCandidates nearest.
Cost is linear in the number of registered interactables, not in the number of physics bodies in your level. That is the whole argument.
PhysicsOverlap (Advanced)
One OverlapMultiByObjectType sphere of GatherRadiusCm against GatherObjectTypes, intersected
with the registry so an overlapped actor resolves to its component without FindComponentByClass.
Cost is linear in the number of physics bodies inside the radius — including every piece of scenery that is not an interactable at all.
The measurement
Two independent runs on the same 8-player dense test map (L_UKInteractorDense, ~2,200 clutter
bodies inside the gather radius), median of 5 after a warm-up:
| Source | First measurement (5.6 / 5.7 / 5.8) | Second measurement (5.8, dedicated perf run) |
|---|---|---|
Registry | 1.6 µs per gather | 7.5 µs median_gather, median_steady 3.9 µs, max 19.9 µs (each figure the worst of the three topologies) |
PhysicsOverlap | 725.6 µs per gather at 2,265 hits | 743.8 µs median_gather, max 810.9 µs |
Aim | 8.6 µs per tick | — |
The per-frame client budget is 200 µs (detection_budget_ms = 0.2), of which the interactor
accepts ≤ 190 µs with 10 µs reserved for the interaction protocol. Registry's worst measured
case sits at 10.5 % of that. PhysicsOverlap is over budget by roughly 3.7× on this map —
by design: it is an opt-in source for a different content shape, not a default that must fit
the budget on every map.
Both sets of numbers are uncooked editor -game figures and are therefore an
upper bound on the shipped cost.
So when would you use PhysicsOverlap?
When your interactables are numerous enough that walking the registry is worse than an overlap
query — a level with thousands of registered interactables and a small gather radius. That is a real
shape, and it is why the source exists. It is not the shape most projects have. Measure before you
switch, on your content, with UK.Detection.Profile 1.
PhysicsOverlap needs collision you author — the kit injects nothing
This is a deliberate non-feature: the plugin adds no
collision object type, no collision channel and no collision preset to your project. It ships no
Config/ row that would inject one.
A plugin that wrote a collision object type into your project's settings would be editing a project-global table that your team owns, that your existing content is already authored against, and that a plugin cannot safely renumber. So the kit does not.
What that means for you, if and only if you set GatherSource = PhysicsOverlap:
- Every interactable actor you want gathered must have a query-enabled primitive component
(
Query OnlyorCollision Enabled, notNo Collisionand not physics-only). - That primitive's object type must be one of
GatherObjectTypes. The defaults (WorldStatic,WorldDynamic,PhysicsBody,Pawn) cover the stock object types; if your project defines a custom object type for interactables, add it toGatherObjectTypeson the profile.
An interactable that fails either test is simply never gathered under this source — it is not an
error, and with the default Registry source it is not even relevant. UK-VAL-033 is the
validator rule that names this gap in the editor before Play
(validator.md); at runtime the symptom is an interactable that the Registry
source finds and the PhysicsOverlap source does not.
Line of sight
- The trace runs from the pawn's eyes (
GetActorEyesViewPoint) to the interactable'sGetInteractionPoint(), onLosTraceChannel, ignoring both the target's actor with its attachments and the pawn with its attachments. - The eyes are also the origin the server uses for its reachability re-check, so client and server verdicts coincide by construction rather than by tuning.
- At most 2 traces per tick. Verdicts are memoised per pool entry for one gather interval and
the walk resolves
Nunknown candidates in⌈N/2⌉ticks, current focus first. - A freshly blocked focus is held for
LosGraceSecondsunless a clear candidate wins outright on usability orPriority. - An interactable whose preset sets
bRequiresLineOfSight = falsecosts no trace at all.
The complex-only wall — the one failure mode worth knowing
bTraceComplex defaults to false, so the LOS trace uses simple collision. A mesh whose
collision is authored complex-only (Use Complex Collision As Simple, or a mesh with per-poly
collision and no simple primitives) has no simple collision at all, so a simple trace passes
straight through it.
The symptom: an interactable stays focusable through a wall. The wall looks solid, blocks the player, and blocks nothing the kit traces.
Three ways out, in the order you should try them:
- Give the blocker simple collision. A box or convex hull on the wall mesh is cheaper for every trace in your game, not just this one, and is almost always the right answer.
- Put blockers on a channel and point
LosTraceChannelat it. If your project already has aVisibility-like channel that blockers respond to, use it. - Set
bTraceComplex = trueon the profile. This works and is the expensive option: complex traces are per-triangle. Measure it.
This is not hypothetical — the kit's own test map carries a complex-only wall precisely to discriminate the two trace modes.
One hazard to know about: BaseEyeHeight
Because the origin is the pawn's eyes, the client's LOS and the server's reachability re-check agree
only while eye height is the same on both machines. BaseEyeHeight is a class default and is
not replicated, so a runtime change on one machine only — a crouch that writes
BaseEyeHeight directly, a vehicle or seat that lowers it, a camera rig that adjusts it locally —
desynchronises the two origins and can produce a server-side NoLineOfSight rejection on an
interaction the client saw as clear. If your pawn changes eye height at runtime, change it on
every machine (a class default per pawn state, a replicated property, or a BeginPlay write on
both) rather than only where the camera lives.
Diagnostics
GetLastDetectionStats()returnsFUKDetectionStats— cumulative counters (TickCount,GatherCount,FocusChangedCount,OriginFallbackCount) and the per-tick values (PoolSize,CandidateCount,RejectedByReason[],LosTraces,OverlapHits,SweepCount,DetectionMicroseconds, …). Cumulative counters keep counting; per-tick values are replaced every tick.UK.Detection.Profile 1turns on the stopwatch that fillsDetectionMicroseconds. It is off by default and costs nothing while off.- Focus changes log at
VerboseonLogUKInteraction.
The validator rules this system contributes
Three are Warnings checked in both halves — before Play by the kit's editor validator (validator.md), and at runtime:
| ID | Condition | Where |
|---|---|---|
UK-VAL-030 | Two UK Interactor components on one pawn — both stay Inert | Editor, before Play (a pawn placed in the level); runtime, on the owning client |
UK-VAL-031 | No detection profile resolvable — class defaults used | Editor, before Play (a pawn placed in the level); runtime, once |
UK-VAL-034 | The player controller's InputComponent is not a UEnhancedInputComponent | Editor, before Play (the project setting); runtime, once (see Docs/input.md) |
A pawn your GameMode spawns at Play is not in the editor's level, so only the runtime line covers it (validator.md).
Three are editor-validator rules, checked before Play by the kit's editor validator (validator.md — the page they appear on and the settings that control it):
| ID | Condition | Note |
|---|---|---|
UK-VAL-032 | InteractionRange > GatherRadiusCm | Clamped at runtime; the asset is the defect |
UK-VAL-033 | PhysicsOverlap source: collision not query-enabled, or object type not in GatherObjectTypes | The collision gap above |
UK-VAL-035 | Slack invariant violated: GatherRadiusCm − InteractionRange ≥ v_ref × GatherIntervalSeconds | A pawn can outrun the gather cadence into interaction range |
The slack invariant is the one worth internalising: the pool is rebuilt only every
GatherIntervalSeconds, so the gather radius must exceed the interaction range by at least the
distance a pawn covers in one interval, or a fast pawn can arrive inside interaction range of an
interactable that is not yet in the pool.
Related
Docs/input.md— the Enhanced Input binding,UK-VAL-034and thebAddMappingContextedge case.Docs/movement.md— the velocity hook and the Mover override recipe.Docs/testing.md— running the kit's own suite in your project.