Skip to main content

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

  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

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
-> Aggregator: Combine weighted scores (default: weighted average)
-> Resolver: Select best (default: lowest score wins)
|
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 context
  • float 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 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 FERPAwarenessContext& 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 (weighted average):
A: (0.6 + 0.2) / 2 = 0.4
B: (1.0 + 0.1) / 2 = 0.55
-> A wins (lower average)

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: Computes the weighted average (sum of weighted scores / number of scorers). This keeps scores normalized regardless of how many scorers are configured.

If no Aggregator is configured, the pipeline uses weighted average automatically. The built-in UERPWeightedAverageAggregator matches this default behavior explicitly.

You can create custom aggregators for sum, 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

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, aggregator, resolver
  • 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:

  • 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.