Pipeline System
Deep dive into the ElysAwareness's modular Pipeline architecture.
Overview
The Pipeline is a modular, data-driven architecture for evaluating perception candidates. Instead of hardcoded perception logic, you configure a series of stages that can be mixed and matched.
Core Philosophy
- Data Over Code: Behavior is configured, not coded
- Immutable Context: All stages read from same snapshot
- Pure Functions: Stages evaluate without modifying state
- Independence: Multiple channels run in parallel
- Opt-In: Targets choose to participate via interfaces
Pipeline Flow
Every tick (controlled by PerceptionTickInterval), the perception component:
- Builds Context - Snapshot of player position, view direction, viewport info
- Runs Each Channel - Target, Interaction, custom channels independently
- Fires Events - Notifies domain components and listeners of changes
Execution Flow
UERPAwarenessComponent::TickComponent()
|
v
BuildContext()
-> Get owner actor (ContextActor)
-> Get view point (position, rotation)
-> Extract aim ray
-> Calculate viewport data + ScreenProjectionPC
-> Create FERPAwarenessContext
|
v
For each channel:
|
v
Execute Pipeline:
-> Sampler: Gather candidates (cached per-frame)
-> Filters: Reject invalid
-> Scorers: Assign weighted scores
-> Aggregate: Combine weighted scores (built-in weighted average)
-> Resolve: Select best (built-in, configurable via ResolverPolicy + StickyBias)
|
v
Store result (best candidate + score)
|
v
Fire OnChannelEvaluated (ALL scored candidates)
|
v
Fire Events (OnChannelCandidateAcquired/Lost)
|
v
Domain components react (OnDomainCandidateChanged)
Pipeline Stages
Stage 1: Sampler
Purpose: Collect potential candidate actors from the world.
Input:
FERPAwarenessContext- Perception contextfloat Range- Maximum detection range
Output:
TArray<AActor*>- All candidate actors
Signature:
void Sample(const FERPAwarenessContext& Context, float Range, TArray<AActor*>& OutCandidates) const;
Built-in Implementations:
UERPSphereOverlapSampler- Sphere overlap query (spatial, range-limited)UERPBoxOverlapSampler- Box overlap query (non-uniform spatial areas)UERPRegistrySampler- Registry-based query (non-spatial, declarative)
Registry Sampler
The Registry Sampler reads from the UERPCandidateRegistrySubsystem (a WorldSubsystem) instead of performing physics queries. Actors register themselves for specific channels using UERPRegistrationComponent or by calling RegisterActor() directly.
This is ideal for:
- Map markers — cities, POIs, quest objectives at any distance
- Tracked actors — party members, waypoints, fast travel points
- Non-spatial perception — any set where actors opt-in declaratively
Registration is local to each machine. Replicated actors register on all clients automatically via standard UE replication — no custom networking needed.
// On any actor — drop a UERPRegistrationComponent and set ChannelIds
// Or register manually:
UERPCandidateRegistrySubsystem* Registry = GetWorld()->GetSubsystem<UERPCandidateRegistrySubsystem>();
Registry->RegisterActor("MapMarkers", this);
By default, the sampler reads candidates for the pipeline's own ChannelId. Set RegistryChannelId to override (e.g. multiple pipelines reading from the same registry channel).
Sampler Caching: Results are cached per-frame per context actor. Pipelines sharing the same sampler instance (or using the same SamplerCacheId) avoid redundant overlap queries. Note: caching applies to spatial samplers; the registry sampler reads directly from the subsystem (which is already O(1) lookup).
Stage 2: Filters
Purpose: Eliminate invalid candidates. ALL filters must pass.
Signature:
bool Passes(const FERPAwarenessContext& Context, AActor* Candidate) const;
Default Implementations:
UERPTargetableFilter- Requires IERPTargetable interfaceUERPInteractableFilter- Requires IERPInteractable interfaceUERPDistanceFilter- Range-based filteringUERPImplementsInterfaceFilter- Generic interface check
Filters run in sequence with short-circuit evaluation. If any filter returns false, the candidate is rejected immediately.
Stage 3: Scorers (with Weights)
Purpose: Assign numerical scores to candidates. Each scorer has an associated weight.
Configuration:
Scorers are configured as FERPScorerEntry structs, each containing:
Scorer- The scorer instance (e.g.,ERPDistanceScorer)Weight- Multiplier applied to the score (default: 1.0)
The raw score from each scorer is multiplied by its weight before aggregation.
Signature:
float Score(const FERPAwarenessContext& Context, AActor* Candidate) const;
Default Implementations:
UERPDistanceScorer- Scores by distance (lower = closer = better)UERPConeScorer- Scores by angle from forwardUERPDotProductScorer- Scores by dot product with a configurable directionUERPViewportEllipseScorer- Scores by screen position
Score Convention:
- Lower scores are better (by convention)
- Typically normalized to 0.0-1.0
ERPPipelineScore::RejectScorerejects the candidate entirely
Example with weights:
Candidate A:
Distance score: 0.3 x weight 2.0 = 0.6
Angle score: 0.2 x weight 1.0 = 0.2
Candidate B:
Distance score: 0.5 x weight 2.0 = 1.0
Angle score: 0.1 x weight 1.0 = 0.1
Aggregated (weighted average):
A: (0.6 + 0.2) / 2 = 0.4
B: (1.0 + 0.1) / 2 = 0.55
-> A wins (lower average)
Aggregation (Built-In)
After scoring, the pipeline automatically computes the weighted average of all scorer outputs for each candidate (sum of weighted scores / number of scorers). This keeps scores normalized regardless of how many scorers are configured.
Aggregation is always weighted average -- there is no separate aggregator class to configure.
Resolution (Built-In)
Purpose: Select the winning candidate from all scored candidates.
Resolution is configured directly on the FERPChannelPipeline struct via two properties:
| Property | Type | Default | Description |
|---|---|---|---|
ResolverPolicy | EERPResolverPolicy | LowestScore | Whether the lowest or highest aggregated score wins |
StickyBias | float | 0.1 | Hysteresis bias to prevent flickering between similar candidates |
ResolverPolicy controls which candidate is selected:
LowestScore-- the candidate with the lowest aggregated score wins (default, good for distance-based scoring)HighestScore-- the candidate with the highest aggregated score wins (good for priority-based scoring)
StickyBias adds hysteresis: the current best candidate gets a score bonus, so a new candidate must beat it by at least StickyBias to take over. Set to 0.0 to disable stickiness.
Context System
FERPAwarenessContext
The context is an immutable snapshot of perception data, built once per tick and shared across all channels.
Contents:
struct FERPAwarenessContext
{
FName ChannelId; // Channel being evaluated (set per-channel)
AActor* ContextActor; // Owner (player pawn/controller)
FVector Origin; // Player position
FVector Forward; // Facing direction
// Optional aim ray
bool bHasAimRay;
FVector AimOrigin;
FVector AimDirection;
// Optional viewport info
bool bHasViewport;
FVector2D ViewportSizePx;
// Optional screen reference (normalized 0..1)
bool bHasScreenReference;
FVector2D ScreenReferenceNdc;
// PlayerController for screen projection (viewport scorers)
APlayerController* ScreenProjectionPC;
};
Note: ChannelId is set automatically per-channel by the perception component — pipeline elements can use it to know which channel is being evaluated (e.g. the UERPRegistrySampler uses it to read from the correct registry channel). ContextActor always points to the owning actor (never overwritten). ScreenProjectionPC is stored separately for viewport-dependent scorers.
Multi-Channel Support
Channel Independence
Each channel runs its own complete pipeline:
- Own sampler, filters, scorers, resolver policy, sticky bias
- Own best candidate
- Own events
Shared Context
While channels are independent, they share the same base context snapshot per tick. The ChannelId field is set per-channel before pipeline evaluation.
Domain Components
Each channel typically has a corresponding domain component:
UERPTargetingComponentfor targeting channelsUERPInteractionComponentfor interaction channels- Custom
UERPDomainComponentsubclasses for custom channels
Domain components auto-bind to perception events and filter by ChannelId.
Performance
Tick Interval
Set PerceptionTickInterval on the perception component to control evaluation frequency:
0.0= every frame (default)0.1= 10 Hz0.2= 5 Hz
Sampler Caching
Sampler results are cached per-frame per context actor. Multiple channels sharing the same sampler instance avoid redundant world queries.
Use SamplerCacheId on the pipeline to force cache sharing across channels with different sampler instances.
Optimization Tips
Reduce Sampling Range: Keep DefaultSamplingRange reasonable.
Use Filters Early: Reject candidates before expensive scoring.
Lightweight Scorers: Keep scoring logic simple.
Scorer Weights: Instead of multiple scorers at weight 1.0, consider if a single well-tuned scorer suffices.
Next Steps
Learn how to create custom pipeline components in Customization.
Create custom domains with Custom Domains.
Implement interactions in Interaction Guide.
Check complete API in API Reference.