Skip to main content

Customization Guide

Create custom pipeline components and extend ElysAwareness for advanced use cases.

When to Customize

Pipeline components (samplers, filters, scorers): when default components don't fit your needs.

Domain components: when you need a new gameplay system that reacts to perception. See Custom Domains for a dedicated guide.

Extension Points

The three configurable pipeline stages each have an abstract base class you can extend in Blueprint or C++:

  • UERPSamplerBase - Custom candidate gathering
  • UERPFilterBase - Custom filtering logic
  • UERPScorerBase - Custom scoring logic

Aggregation (weighted average) and resolution (lowest/highest score + sticky bias) are built into the pipeline struct and configured via properties -- no custom classes needed.


Custom Samplers

Purpose

Samplers gather potential candidates. The plugin ships with spatial samplers (sphere/box overlap) and a registry sampler (declarative). Customize when you need:

  • Different spatial queries (raycast, cone sweep)
  • Specialized non-spatial queries beyond the built-in registry
  • Cached/optimized candidate lists

Interface

UFUNCTION(BlueprintNativeEvent, Category = "Pipeline")
void Sample(const FERPAwarenessContext& Context, float Range, TArray<AActor*>& OutCandidates) const;

C++ Example: Line Trace Sampler

UCLASS(Blueprintable, EditInlineNew, DefaultToInstanced)
class YOURGAME_API UERPLineTraceSampler : public UERPSamplerBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere) TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;

virtual void Sample_Implementation(const FERPAwarenessContext& Context, float Range,
TArray<AActor*>& OutCandidates) const override
{
if (!Context.ContextActor) return;
UWorld* World = Context.ContextActor->GetWorld();
if (!World) return;

FCollisionQueryParams Params;
Params.AddIgnoredActor(Context.ContextActor);

FHitResult Hit;
if (World->LineTraceSingleByChannel(Hit, Context.Origin,
Context.Origin + Context.Forward * Range, TraceChannel, Params))
{
if (AActor* A = Hit.GetActor()) OutCandidates.Add(A);
}
}
};

Custom Filters

Purpose

Filters reject invalid candidates. Customize for:

  • Line of sight checks
  • Team/faction filtering
  • State validation (alive, active)

Interface

UFUNCTION(BlueprintNativeEvent, Category = "Pipeline")
bool Passes(const FERPAwarenessContext& Context, AActor* Candidate) const;

C++ Example: Line of Sight Filter

UCLASS(Blueprintable, EditInlineNew, DefaultToInstanced)
class YOURGAME_API UERPLineOfSightFilter : public UERPFilterBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere) TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;

virtual bool Passes_Implementation(const FERPAwarenessContext& Context,
AActor* Candidate) const override
{
if (!Context.ContextActor || !Candidate) return false;
UWorld* World = Context.ContextActor->GetWorld();
if (!World) return false;

FCollisionQueryParams Params;
Params.AddIgnoredActor(Context.ContextActor);
Params.AddIgnoredActor(Candidate);

FHitResult Hit;
bool bHit = World->LineTraceSingleByChannel(Hit, Context.Origin,
Candidate->GetActorLocation(), TraceChannel, Params);
return !bHit;
}
};

Custom Scorers

Purpose

Scorers assign numerical values. Each scorer is paired with a weight in the pipeline configuration (FERPScorerEntry).

Interface

UFUNCTION(BlueprintNativeEvent, Category = "Pipeline")
float Score(const FERPAwarenessContext& Context, AActor* Candidate) const;

Convention: Lower score = better. Return ERPPipelineScore::RejectScore to reject.

C++ Example: Health Priority Scorer

UCLASS(Blueprintable, EditInlineNew, DefaultToInstanced)
class YOURGAME_API UERPHealthScorer : public UERPScorerBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere) bool bPrioritizeLowHealth = true;

virtual float Score_Implementation(const FERPAwarenessContext& Context,
AActor* Candidate) const override
{
UHealthComponent* HC = Candidate->FindComponentByClass<UHealthComponent>();
if (!HC) return 1.0f;
float HP = HC->GetHealthPercent();
return bPrioritizeLowHealth ? HP : (1.0f - HP);
}
};

Using Weights

Configure in the pipeline:

Scorers:
- ERPDistanceScorer, Weight: 2.0 (distance matters more)
- ERPHealthScorer, Weight: 1.0 (health is secondary)
- ERPConeScorer, Weight: 0.5 (angle is tertiary)

Each raw score is multiplied by its weight before aggregation.


Aggregation and Resolution

Aggregation and resolution are built into the pipeline and configured via properties on FERPChannelPipeline:

PropertyTypeDefaultDescription
ResolverPolicyEERPResolverPolicyLowestScoreWhether the lowest or highest aggregated score wins
StickyBiasfloat0.1Hysteresis bias to prevent flickering between similar candidates

Aggregation is always weighted average (sum of weighted scores / number of scorers). There is no custom aggregator class.

Resolution selects the winner based on ResolverPolicy:

  • LowestScore -- candidate with the lowest aggregated score wins (good for distance-based scoring)
  • HighestScore -- 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.


Best Practices

  • Keep Stages Pure: Don't modify candidates or context
  • Normalize Scores: Use 0.0-1.0 range when possible
  • Handle Edge Cases: Null checks, empty arrays, invalid context
  • Optimize: Filters run per-candidate, keep fast. Cache expensive calculations.
  • Document Parameters: Use UPROPERTY meta tags and tooltips

Next Steps

Create custom domains with Custom Domains.

Check complete API in API Reference.

See Quick Reference for cheat sheet.