Systems · Attention selection

Interest Engine

What makes an intelligent agent notice something, and ignore everything else?

A standalone C++20 kernel that picks one attention target from the candidates a game supplies, and records why. A native adapter runs it inside Unreal Engine.

Watch “Interest Engine | Context-Aware Attention Selection in Unreal Engine 5.8” on YouTube (opens in new tab)
Showcase · Unreal Engine 5.8 · 66 seconds of runtime captureWatch on YouTube (opens in new tab)

Authored scene, live decisions. The park, its characters and every event are scripted. What the observer attends to is not: each target is the kernel’s own decision at runtime, ten decisions a second.

The boundary

The game perceives. The kernel chooses.

Interest Engine is not a perception system. It does not find actors, trace lines of sight, interpret a scene or animate a character. The host application does that work and describes what it perceived as candidates: stable IDs, positions, attraction, salience, perception strength, tags and recent events.

The kernel weighs those candidates against the observer’s context and its previous choice, and returns one target or none, with the reason. What happens next, a head turn, a line of dialogue, a change of plan, belongs to the host again.

Host

Perceive

Finds candidates and describes them: IDs, positions, tags, salience, events. Supplies the time.

Interest Engine

Kernel

Score

Context preference, distance and angle gates, perception strength, event novelty.

Kernel

Decide

Acquire, Keep, Switch, Clear or None, with a reason and an optional trace.

Host

Act

Turns the chosen ID into gaze, animation or gameplay. In the showcase: head and eye motion.

How it decides

Four stages, one target.

Every step, every candidate takes the same path. There is no randomness, and an optional trace records the values each decision used.

  1. Preference

    weighted mean of attraction, salience and tag affinity

    How interesting a candidate is to this observer, now. The observer’s context decides how much each tag matters: in the showcase the performer’s affinity is 0.90 while the observer sits and 0.10 once it walks.

  2. Gates

    raw = preference × distance × angle × perception

    Distance falloff, the angle from the observer’s body or travel direction, and the host’s perception strength turn preference into a raw score between 0 and 1.

  3. Novelty

    effective = min(1, raw + beta × noveltyGate × eventNovelty) eventNovelty = strength × 2^(−age / halfLife)

    A timestamped host event adds a boost that halves every half-life. The boost has its own novelty gate: distance and perception always apply, while the event’s directionality sets how much the angle counts, from fully (1, the default) to not at all (0). The result is capped at 1. The crates’ crash is submitted at directionality 0, so novelty lifts their raw score of 0.07 to 0.87.

  4. Temporal policy

    acquire 0.50 · release 0.35 · switch +0.12

    A target is acquired above one threshold and kept until it falls below a lower one. A challenger has to lead by a margin, and an optional minimum dwell protects a new choice. Small fluctuations stop causing constant switching.

Every step returns

Decision
  • Acquire
  • Keep
  • Switch
  • Clear
  • None
Target
The selected candidate’s ID, or 0 for no target
Reason
  • HighestScore
  • BetterCandidate
  • InsufficientAdvantage
  • MinimumDwell
  • DwellOverride
  • AboveReleaseThreshold
  • BelowReleaseThreshold
  • BelowAcquireThreshold
  • CurrentInvalid
  • NoEligibleCandidate
Trace
Optional: the values the scoring and policy actually used, per candidate

Explainable

Every decision can say why.

At 7.6 seconds the jogger’s score passes the performer’s. The lead stays under the switch margin, so the kernel keeps the performer, and the showcase panel says so.

Showcase frame at 8.4 seconds. The seated observer keeps looking at the performer while the jogger runs between them. The panel reads: Looking at Performer. Decision at 8.4 s: keeps Performer, the jogger leads by 0.068, less than the 0.12 switch margin. Live scores: Jogger 0.63, Performer 0.56, Crates 0.07.
  1. Looking at

    The current target: the kernel’s selected ID, resolved to a name by the host.

  2. Decision at 8.4 s

    One kernel step in words. Keep, reason InsufficientAdvantage: the jogger leads by 0.068, under the 0.12 switch margin.

  3. Live scores

    Effective scores with acquire and release marked. The panel only restates kernel output; it computes nothing.

The run

Sixty-six seconds, 666 decisions.

The showcase as the kernel saw it, drawn from the run’s own per-step output. Thin lines are every candidate’s effective score; the accent line and the track show the target it chose.

Effective scores and selected target over the 66.5 second showcase runEight candidates are scored ten times per second. Acquire threshold 0.50, release threshold 0.35. The observer is seated until 26.0 s, then walking. Host events: Crates at 17.0 s, Crates at 19.2 s, Crows at 42.0 s, Crow at 53.5 s. Selection changes: 4.0 s Acquire Performer; 13.5 s Clear (Performer); 17.0 s Acquire Crates; 21.8 s Clear (Crates); 40.0 s Acquire Fox; 42.0 s Switch Crows; 47.8 s Clear (Crows); 52.5 s Acquire Crow.Context · Seated, relaxedContext · Walking, purposeful0.00.51.0scoreacquire 0.50release 0.35PerformerJoggerCratesStrollerFoxCrowsPedestrianCrowPerformerCratesCrowsCrowtarget0 s10 s20 s30 s40 s50 s60 s66.5 s
  • Selected target
  • Other candidates
  • Acquire 0.50
  • Release 0.35
  • Host event

Scroll the chart sideways to see the whole run.

  1. A stack of wooden crates tipping over behind the park bench where the observer sits, as the jogger runs past.

    17.0 sAcquire · Crates

    An event off to the side

    The crates topple behind the bench, far from where the observer faces. Their raw score is 0.07, but the host submits the crash as an event with angle-independent novelty. The kernel acquires them at 0.87 and holds them above release until 21.8 s.

    Run logCrates: event novelty +0.80 on a raw score of 0.07 (angle gate 0.18) -> 0.87

  2. The observer walks along a park path seen from behind; a fox stands in the grass beside the path, with crows on the ground ahead.

    40.0 sAcquire · Fox, then Switch · Crows

    Ignored, then noticed

    Walking now, the observer passes the fox. Its score only reaches 0.49, just under the 0.50 acquire threshold, so it stays ignored. When it bolts at 40.0 s the score jumps to 0.53 and the kernel acquires it. Two seconds later the crows take off; their event lifts them to 1.00 and the kernel switches.

    Run logCrows: event novelty +0.75 on a raw score of 0.58 -> 1.00

  3. The observer stands at the end of the path, facing a crow perched on a low wall in the park.

    52.5 sAcquire · Crow, held to the end

    Held, not dropped

    A single crow glides in and lands on the low wall ahead. As its novelty fades the score slips below acquire but stays above release, so the kernel keeps it through the last frame instead of looking away.

    Run logKeeps Crow at 0.498: below acquire 0.50 but above release 0.35

Temporal stability

Same scores. Different attention.

The same scenario, run twice through the same kernel, with identical candidate scores on every step. Only the temporal policy differs. The greedy baseline follows whatever leads above acquire: release equals acquire, no minimum dwell, a 0.001 switch margin. The stable profile keeps its 0.35 release, 0.12 switch margin and minimum dwell.

A target change is an acquire, a switch or a clear. The stable run makes 4 acquires, 1 switch and 3 clears; the baseline makes 4, 3 and 4. The 91 steps that select differently come from two mechanisms: 75 where the release threshold keeps a target the baseline has dropped, and 16 where the 0.12 switch margin keeps the current target against a challenger that leads by less than that. The minimum dwell is part of the profile but decided no step in this run.

Target changes, stable profile
8
Target changes, greedy baseline
11
Steps that select differently
91 of 666
Selected target per step, stable profile and greedy baseline91 of 666 steps select differently. Stable profile: 4.0 s Acquire Performer; 13.5 s Clear; 17.0 s Acquire Crates; 21.8 s Clear; 40.0 s Acquire Fox; 42.0 s Switch Crows; 47.8 s Clear; 52.5 s Acquire Crow. Greedy baseline: 4.0 s Acquire Performer; 7.6 s Switch Jogger; 9.2 s Switch Performer; 13.5 s Clear; 17.0 s Acquire Crates; 20.2 s Clear; 40.0 s Acquire Fox; 42.0 s Switch Crows; 46.9 s Clear; 52.5 s Acquire Crow; 61.6 s Clear.shaded: 91 of 666 steps differStable profilePerformerCratesCrowsCrowGreedy baselineCrowsCrow0 s10 s20 s30 s40 s50 s60 s66.5 s
Scroll the tracks sideways to see the whole run.
Watch “Interest Engine | Greedy Selection vs Temporally Stable Attention” on YouTube (opens in new tab)
Policy comparison · real kernel output, both configurationsWatch on YouTube (opens in new tab)
Where the two configurations differ
MomentStable profileGreedy baseline
The jogger outranks the performer7.6–9.2 sKeeps the performerSwitches to the jogger, then back
Crate novelty fades20.2–21.7 sHolds the crates until 21.8 sDrops them at 20.2 s
Crow take-off novelty fades46.9–47.7 sHolds the crows until 47.8 sDrops them at 46.9 s
The landed crow61.6–66.5 sHolds it to the endClears at 61.6 s and looks at nothing

Both sides are the same native kernel. The baseline stands for the common rule of looking at the highest score above a threshold; it is not a model of any particular engine’s look-at system. The footage shows the stable profile only. In the video, A is the greedy baseline and B is the M4 profile, the stable profile here. Which behaviour is better is a design choice: the comparison measures what changes.

Engineering

Small, explicit and testable.

The kernel owns scoring, selection and temporal policy. Everything about the world, the clock and the character stays with the host.

C++ kernel

v0.2.0 · APACHE 2.0
Language
C++20, standard library only
Footprint
Two headers, two implementation files
State
Explicit host time and caller-owned memory. No hidden clock, RNG or global state.
Allocation
None in prepared Step calls
Failure
A failed step leaves memory, decision and trace untouched. Invalid input is rejected, not repaired.
Determinism
Same inputs, same decisions, within a tested build and platform
Tested on
Windows x64 MSVC; Linux x86-64 GCC and Clang; Debug and Release; Clang ASan and UBSan
License
Apache 2.0 · v0.2.0

Unreal adapter

v0.2.1 · BETA
Engine
Unreal Engine 5.8, native C++ Runtime plugin
Surface
One module, one public header. No UObjects, Blueprints, editor modules or ticking.
Kernel
Vendored unchanged, built with precise floating point and checked at compile time
Ownership
Owned profile storage and prepared, reusable workspaces
Hot path
Step forwards straight to the kernel and does not allocate
Validated
Win64 only, on Unreal Engine 5.8.4: Editor Development, Game Development and Shipping builds, each unity and non-unity (6 configurations), and 8 adapter automation tests in the Editor
Status
Beta · v0.2.1 · Apache 2.0
Standalone kernel API · one step. The Unreal adapter wraps it as FStepWorkspace::Step
interest::Decision decision{};
const auto status = interest::Step(observer, candidates, profile, now,
                                   memory, scratch, decision, &trace);
if (status == interest::StepStatus::Ok) {
    // decision.kind:     Acquire, Keep, Switch, Clear or None
    // decision.selected: the chosen candidate ID, 0 for none
    // decision.reason:   for example InsufficientAdvantage
}

The showcase

The Unreal project feeds the kernel through the adapter and never edits its decisions. Head and eye motion are presentation only and never flow back into the kernel’s input.

Cadence
10 steps/s, host time
Tests
46 Showcase automation tests pass
Trace
Byte-identical across runs
Engine
Unreal Engine 5.8

Source

Read the code.

Unreal adapter

PUBLIC · BETA

InterestEngineUnreal

A native Unreal Engine 5.8 adapter built on the same C++20 kernel demonstrated in the showcase, adding conversions, owned profile storage and prepared workspaces without changing the kernel. Version 0.2.1 is a beta for Win64, verified on Unreal Engine 5.8.4, under the Apache 2.0 license.

Attention isn’t just about what you see.

It’s about what you choose to notice.

Watch the showcase again