Source code: NGInteractionSystem on GitHub — MIT license, free to use.
Why I Built This
Every game needs an interaction system eventually. You walk up to a chest, a door, an NPC — and press a button. Sounds simple, but the moment you start implementing it, questions pile up. How do you detect what’s nearby? How do you pick the right object when multiple are in range? How do you keep it decoupled so any actor can be interactable without inheriting from some massive base class?
I wanted something reusable. A small plugin I could drop into any project, wire up in an hour, and forget about. No dependencies on a specific game framework, no assumptions about what you’re interacting with.
The Architecture
The system splits interaction into two sides with an interface each:
INGInteractionInterface— implemented by anything the player can interact with (a pickup, a door, a lever)INGCharacterInteractInterface— implemented by whoever does the interacting (the player character, or potentially an AI)
Both are Unreal interfaces — abstract contracts that decouple the two sides completely.
The glue between them is UNGCharacterInteractComponent — a scene component you attach to your character. It handles proximity detection, target selection, and delegates the actual interaction through the interface.
This separation matters. The character doesn’t need to know what it’s interacting with. The interactable doesn’t need to know who’s interacting with it. They communicate through a contract — the interface — and nothing else.
The Interface
INGInteractionInterface has five methods:
Interact(AActor* InteractingActor)— the actual interaction, called when the player presses the buttonIsReadyToInteract()— returns whether the object is currently available (useful for cooldowns, locked doors, etc.)ReadyToInteract()— notification that the player is looking at this objectSelectedToInteract()— notification that this is now the active selectionNotReadyToInteract()— notification that the player looked away
All are BlueprintNativeEvent, so you can implement them in C++ or Blueprints.
The Detection: Box + Angle Cone
The detection works in two phases:
Phase 1 — Spatial filtering (event-driven). The component creates a UBoxComponent trigger (default: 100x50x50 units). When an actor enters the box, the overlap event fires, and if that actor implements INGInteractionInterface, it gets added to a tracked list. When it leaves — removed. This is cheap. The physics engine handles it, no per-frame cost for discovery.
Phase 2 — Angular filtering (periodic). The component periodically iterates over the tracked list and finds the “best” target. It gets the camera’s position and forward vector, then for each candidate calculates the dot product between where the camera points and where the object is. The one closest to the center of the screen — within a configurable threshold (default 15°) — wins.
for (AActor* Actor : OverlappingInteractables)
{
FVector DirectionToActor = (Actor->GetActorLocation() - CameraLocation).GetSafeNormal();
float DotProduct = FVector::DotProduct(CameraForward, DirectionToActor);
float Angle = FMath::RadiansToDegrees(FMath::Acos(DotProduct));
if (Angle <= InteractAngleThreshold && DotProduct > BestDotProduct)
{
BestDotProduct = DotProduct;
BestActor = Actor;
}
}When the best target changes, the old one gets NotReadyToInteract(), the new one gets ReadyToInteract() followed by SelectedToInteract().
The update frequency is configurable via UpdateInterval. Set it to 0 for every-frame updates, or something like 0.1 for 10Hz polling — the player won’t notice a 100ms delay in highlight switching, and it reduces the per-frame work.
Why Tick-Based Detection
The reason the detection runs periodically (rather than only on input) is visual feedback. When the player approaches interactable objects, they need to see which one is currently targeted. The interactable changes its material — it gets highlighted — and the one that’s ready to be interacted with gets a green overlay, telling the player “you can interact with this.” Without periodic updates, you’d lose this feedback entirely.
The base class ANGInteractableActor handles this with overlay materials. ReadyToInteract() applies InteractReadyMaterial as an overlay, SelectedToInteract() applies InteractSelectedMaterial, and NotReadyToInteract() clears it. Overlay materials render on top of the base material without modifying it — the object’s original appearance stays untouched.
Performance
I’ve tested the system with hundreds of objects inside the box collision at once, and it worked flawlessly. I never actually profiled it — it just never caused any issues. The math per object is trivial: a normalization, a dot product, and an acos. Even with hundreds of candidates, this is nothing for the CPU.
How Other Systems Do It
Lyra — Epic’s Official Sample Project
Lyra’s interaction system is fully built on the Gameplay Ability System (GAS). It runs two separate queries on timers (every 0.1s):
First, a UAbilityTask_GrantNearbyInteraction does a sphere trace at 500cm radius to find everything that implements IInteractableTarget. When it finds something, it pre-grants the associated Gameplay Ability to the player’s PlayerState — before the player even looks at it. Second, a UAbilityTask_WaitForInteractableTargets_SingleLineTrace fires a line trace at 200cm to figure out which interactable the player is actually looking at.
The clever part is the 500cm pre-grant radius being 2.5x the actual interaction range. In multiplayer, abilities are already replicated to the server by the time the player gets close enough to press the button. No “grant on interact” latency.
The downside is the weight. You’re pulling in the entire GAS infrastructure — abilities, attribute sets, gameplay effects — for what is fundamentally “press E near a thing.” For a large project that already uses GAS everywhere, this makes sense. For a standalone interaction plugin, it’s overkill.
Mountea Framework — Swappable Detection
Mountea Interaction System takes a different approach: the detection method is a swappable component. You pick InteractorComponent_Trace for a line trace or InteractorComponent_Overlap for collision-based detection, and plug it into the same framework.
It also supports five interaction types — Auto, Press, Hold, Mash, and Hover (for top-down cursor games). Interactables inherit from WidgetComponent so they natively manage their own UI overlays. Configuration is stored in DataTables, so designers can tune everything without touching code.
The swappable interactor design is interesting. If you need to support both first-person and top-down modes in the same project, having trace vs. overlap as a component choice makes a lot of sense.
Hybrid Collision + Trace
A pattern described by Brian Stong that’s architecturally the closest to my approach. A collision volume around the player acts as a gate — when something enters it, a boolean IsInteractableNearby flips to true. Only then does a sphere trace start firing from the camera to find the focused target.
The key optimization is the conditional trace. If nothing is nearby, no per-frame work happens at all. My system does the same thing conceptually — the box overlap is the gate, the angle check is the narrow phase. The difference is I use math instead of a physics trace for the second stage.
Cone Trace Approach
Sam Reitich’s cone trace solves the “detect things in a cone in front of the player” problem with a single sphere sweep followed by mathematical filtering. It uses the dot product to discard hits outside the cone angle — the same math my system uses, but wrapped in a proper trace that returns FHitResult data with hit locations and normals.
Performance clocks in at 7-10 microseconds per call. If I needed actual hit data (exact surface point, normal) rather than just “which actor,” this would be worth integrating.
How It’s Used in The Break of Day
In ArcShooter, the NGInteractionSystem plugin is the foundation for all pickups. The game extends it with a pickup hierarchy:
AASPickupBase— abstract pickup with collision detectionAASWeaponSpawner— weapons with cooldown and respawn, replicated for multiplayerAASAmmoSpawner— ammo via GameplayTag stacksAASLorePickup— lore entries that integrate with a persistent discovery system
The character’s TryToInteract checks IsReadyToInteract() before calling Interact() — for example, a weapon spawner on cooldown returns false, so the player can see it but can’t pick it up yet.
All pickup properties are defined in data assets (UASPickupDefinition), so designers can create new pickup types without touching code. Mesh, effects, sounds, cooldown duration — all configurable in the editor.
What I’d Change
Looking at the code now, a few things I’d do differently:
ReadyToInteract()andSelectedToInteract()fire in the same frame, back-to-back. In practice they collapse into one state. I’d either merge them or add a real delay between the two.- Adding a priority system would help for cases where multiple interactables overlap — letting certain objects (like quest items) take precedence regardless of angle.
But these are improvements, not fixes. The core design — interface-based, component-driven, two-phase detection — holds up well.