Pipeline System
Deep dive into the ElysPerceptionPlugin'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
UERPPerceptionComponent::TickComponent()
|
v
BuildContext()
-> Get owner actor (ContextActor)
-> Get view point (position, rotation)
-> Extract aim ray
-> Calculate viewport data + ScreenProjectionPC
-> Create FERPPerceptionContext
|
v
For each channel:
|
v
Execute Pipeline:
-> Sampler: Gather candidates (cached per-frame)
-> Filters: Reject invalid
-> Scorers: Assign weighted scores
-> Aggregator: Combine weighted scores (default: sum)
-> Resolver: Select best (default: lowest score wins)
|
v
Store result (best candidate + score)
|
v
Fire Events (OnChannelCandidateAcquired/Lost)
|
v
Domain components react (OnDomainCandidateChanged)
Pipeline Stages
Stage 1: Sampler
Purpose: Collect potential candidate actors from the world.
Input:
FERPPerceptionContext- Perception contextfloat Range- Maximum detection range
Output:
TArray<AActor*>- All candidate actors
Signature:
void Sample(const FERPPerceptionContext& Context, float Range, TArray<AActor*>& OutCandidates) const;
Default Implementation:
UERPSphereOverlapSampler- Sphere overlap query
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.
Stage 2: Filters
Purpose: Eliminate invalid candidates. ALL filters must pass.
Signature:
bool Passes(const FERPPerceptionContext& 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 being passed to the aggregator.
Signature:
float Score(const FERPPerceptionContext& Context, AActor* Candidate) const;
Default Implementations:
UERPDistanceScorer- Scores by distance (lower = closer = better)UERPConeScorer- Scores by angle from forwardUERPViewportEllipseScorer- 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 (sum):
A: 0.6 + 0.2 = 0.8
B: 1.0 + 0.1 = 1.1
-> A wins (lower total)
Stage 4: Aggregator
Purpose: Combine multiple weighted scores into a single final score per candidate.
Signature:
float Aggregate(const TArray<float>& Scores) const;
Default behavior (UERPAggregatorBase): Sums all weighted scores.
If no Aggregator is configured, the pipeline falls back to summing the weighted scores directly.
You can create custom aggregators for weighted averaging, min/max selection, or non-linear combinations. See Customization.
Stage 5: Resolver
Purpose: Select the winning candidate from all scored candidates.
Signature:
bool Resolve(const TArray<FERPPipelineScoredCandidate>& ScoredCandidates, FERPPipelineScoredCandidate& OutBest) const;
Default behavior (UERPResolverBase): Selects the candidate with the lowest score.
Context System
FERPPerceptionContext
The context is an immutable snapshot of perception data, built once per tick and shared across all channels.
Contents:
struct FERPPerceptionContext
{
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: 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, aggregator, resolver
- Own best candidate
- Own events
Shared Context
While channels are independent, they share the same context snapshot per tick.
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.