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:

  1. gathers candidate interactables on a time cadence (default every 0.1 s),
  2. filters and scores the pool every frame into exactly one focus, and
  3. 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.

FieldDefaultRangeWhat it does
StrategyHybrid—Hybrid = periodic gather + per-frame scoring + LOS on the leader. Aim (Advanced) = one sweep per tick, and the sweep is the LOS.
GatherSourceRegistry—Registry (default) or PhysicsOverlap (Advanced). See below.
GatherIntervalSeconds0.10.033 – 0.5How often the candidate pool is rebuilt.
GatherRadiusCm1000.0100 – 10000Pool radius around the pawn.
ViewConeHalfAngleDeg60.010 – 180Half-angle of the gather cone. 180 disables the cone.
MaxCandidates164 – 64Pool cap, applied after the cone.
LosTraceChannelVisibility—Trace channel for the line-of-sight check.
GatherObjectTypesWorldStatic, WorldDynamic, PhysicsBody, Pawn—PhysicsOverlap source only.
bTraceComplexfalse—Simple vs complex collision for the LOS trace. See "the complex-only wall".
AimSweepRadiusCm20.00 – 100Aim strategy sweep radius.
DetectionOriginControllerView—Where the aim direction comes from. The LOS origin is always the eyes.
HysteresisAngleDeg5.00 – 30A challenger must beat the current focus by this much on angle.
HysteresisDistanceCm25.00 – 200…and by this much on distance.
FocusDwellSeconds0.10 – 0.5Minimum time a focus is held before hysteresis is even consulted.
LosGraceSeconds0.150 – 0.5A freshly blocked focus is kept this long before it is dropped.
bFocusNonUsabletrue—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:

SourceFirst measurement (5.6 / 5.7 / 5.8)Second measurement (5.8, dedicated perf run)
Registry1.6 µs per gather7.5 µs median_gather, median_steady 3.9 µs, max 19.9 µs (each figure the worst of the three topologies)
PhysicsOverlap725.6 µs per gather at 2,265 hits743.8 µs median_gather, max 810.9 µs
Aim8.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:

  1. Every interactable actor you want gathered must have a query-enabled primitive component (Query Only or Collision Enabled, not No Collision and not physics-only).
  2. 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 to GatherObjectTypes on 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's GetInteractionPoint(), on LosTraceChannel, 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 N unknown candidates in ⌈N/2⌉ ticks, current focus first.
  • A freshly blocked focus is held for LosGraceSeconds unless a clear candidate wins outright on usability or Priority.
  • An interactable whose preset sets bRequiresLineOfSight = false costs 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:

  1. 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.
  2. Put blockers on a channel and point LosTraceChannel at it. If your project already has a Visibility-like channel that blockers respond to, use it.
  3. Set bTraceComplex = true on 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() returns FUKDetectionStats — 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 1 turns on the stopwatch that fills DetectionMicroseconds. It is off by default and costs nothing while off.
  • Focus changes log at Verbose on LogUKInteraction.

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:

IDConditionWhere
UK-VAL-030Two UK Interactor components on one pawn — both stay InertEditor, before Play (a pawn placed in the level); runtime, on the owning client
UK-VAL-031No detection profile resolvable — class defaults usedEditor, before Play (a pawn placed in the level); runtime, once
UK-VAL-034The player controller's InputComponent is not a UEnhancedInputComponentEditor, 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):

IDConditionNote
UK-VAL-032InteractionRange > GatherRadiusCmClamped at runtime; the asset is the defect
UK-VAL-033PhysicsOverlap source: collision not query-enabled, or object type not in GatherObjectTypesThe collision gap above
UK-VAL-035Slack invariant violated: GatherRadiusCm − InteractionRange ≥ v_ref × GatherIntervalSecondsA 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.

  • Docs/input.md — the Enhanced Input binding, UK-VAL-034 and the bAddMappingContext edge case.
  • Docs/movement.md — the velocity hook and the Mover override recipe.
  • Docs/testing.md — running the kit's own suite in your project.