Skip to main content

Customization Guide

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

When to Customize

Pipeline components (samplers, filters, scorers, aggregators, resolvers): 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

Each pipeline stage has an abstract base class you can extend in Blueprint or C++:

  • UERPSamplerBase - Custom candidate gathering
  • UERPFilterBase - Custom filtering logic
  • UERPScorerBase - Custom scoring logic
  • UERPAggregatorBase - Custom score combination
  • UERPResolverBase - Custom winner selection

Custom Samplers

Purpose

Samplers gather potential candidates. Customize when you need:

  • Different spatial queries (raycast, box overlap)
  • Non-spatial queries (GameState actor lists)
  • Cached/optimized candidate lists

Interface

UFUNCTION(BlueprintNativeEvent, Category = "ElysPerception|Pipeline")
void Sample(const FERPPerceptionContext& 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 FERPPerceptionContext& 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 = "ElysPerception|Pipeline")
bool Passes(const FERPPerceptionContext& 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 FERPPerceptionContext& 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 = "ElysPerception|Pipeline")
float Score(const FERPPerceptionContext& 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 FERPPerceptionContext& 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.


Custom Aggregators

Purpose

Aggregators combine weighted scores. The default sums them.

Interface

UFUNCTION(BlueprintNativeEvent, Category = "ElysPerception|Pipeline")
float Aggregate(const TArray<float>& Scores) const;

Note: The Scores array contains already-weighted values.

C++ Example: Min Aggregator

UCLASS(Blueprintable, EditInlineNew, DefaultToInstanced)
class YOURGAME_API UERPMinAggregator : public UERPAggregatorBase
{
GENERATED_BODY()
public:
virtual float Aggregate_Implementation(const TArray<float>& Scores) const override
{
float Min = TNumericLimits<float>::Max();
for (float S : Scores) Min = FMath::Min(Min, S);
return (Min == TNumericLimits<float>::Max()) ? 0.f : Min;
}
};

Custom Resolvers

Purpose

Resolvers select the winner. The default picks lowest score.

Interface

UFUNCTION(BlueprintNativeEvent, Category = "ElysPerception|Pipeline")
bool Resolve(const TArray<FERPPipelineScoredCandidate>& ScoredCandidates,
FERPPipelineScoredCandidate& OutBest) const;

C++ Example: Sticky Target Resolver

UCLASS(Blueprintable, EditInlineNew, DefaultToInstanced)
class YOURGAME_API UERPStickyResolver : public UERPResolverBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere) float StickyBias = 0.1f;
UPROPERTY() mutable TWeakObjectPtr<AActor> LastWinner;

virtual bool Resolve_Implementation(
const TArray<FERPPipelineScoredCandidate>& ScoredCandidates,
FERPPipelineScoredCandidate& OutBest) const override
{
if (ScoredCandidates.Num() == 0) return false;
OutBest = ScoredCandidates[0];
for (const auto& C : ScoredCandidates)
{
float Adjusted = C.Score;
if (C.Actor == LastWinner.Get()) Adjusted -= StickyBias;
if (Adjusted < OutBest.Score) OutBest = C;
}
LastWinner = OutBest.Actor;
return true;
}
};

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.