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 gatheringUERPFilterBase- Custom filtering logicUERPScorerBase- Custom scoring logicUERPAggregatorBase- Custom score combinationUERPResolverBase- 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.