Skip to main content

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

  1. Data Over Code: Behavior is configured, not coded
  2. Immutable Context: All stages read from same snapshot
  3. Pure Functions: Stages evaluate without modifying state
  4. Independence: Multiple channels run in parallel
  5. Opt-In: Targets choose to participate via interfaces

Pipeline Flow

Every tick (controlled by PerceptionTickInterval), the perception component:

  1. Builds Context - Snapshot of player position, view direction, viewport info
  2. Runs Each Channel - Target, Interaction, custom channels independently
  3. 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 context
  • float 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 interface
  • UERPInteractableFilter - Requires IERPInteractable interface
  • UERPDistanceFilter - Range-based filtering
  • UERPImplementsInterfaceFilter - 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 forward
  • UERPViewportEllipseScorer - Scores by screen position

Score Convention:

  • Lower scores are better (by convention)
  • Typically normalized to 0.0-1.0
  • ERPPipelineScore::RejectScore rejects 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:

  • UERPTargetingComponent for targeting channels
  • UERPInteractionComponent for interaction channels
  • Custom UERPDomainComponent subclasses 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 Hz
  • 0.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.