Hello again everyone! Welcome to another entry in this on-going blog/development thread for my GAS Course Project. For more context of this project, please see the very first post in this thread, and as usual, you can find my socials here:
Twitch : Twitch
Discord : UE5 - GAS Course Project - JevinsCherries
Tiktok : Devin Sherry (@jevinscherriesgamedev) | TikTok
Youtube : Youtube
Twitter : https://twitter.com/jevincherries
Instagram : Jevins Cherries Gamedev (@jevinscherriesgamedev) • Instagram photos and videos
Today we are going to talk about the basic principles of combat targeting and how we can use these principles with the Targeting System plugin from Epic to create robust targeting behavior that fits the style of an Action RPG. I will showcase custom target sort classes and debugging tools that I created to give an example projectile ability targeting that feels good and better captures player intent in combat.
Classes to Research:
UTargetingTask
UTargetingPreset
UTargetingSubsystem
Additional Reading:
Principles of Combat Targeting
Targeting can differ depending on the type of combat your game will feature; melee, ranged, a mix of both, etc. However, I have found that there a few basic principles that, when used in different contexts or combinations, will give you targeting results that better match the intent of the player.
Distance to Target
This principle is pretty straight-forward; how close am I to a specific target? In most cases, we’d want to use the horizontal distance between the player and the target, ignoring the Z-axis (vertical) unless your combat system emphasizes verticality, and in the case, your targeting philosophy should reflect that. In my experience, this alone will not give you desired targeting results, but can influence the general targeting weight under certain circumstances. For example, if you have two targets that are relatively close to each other in screen-space but once is signficantly closer, most times it makes sense to choose the closer target. This makes sense for cases such as melee combat, but maybe for ranged combat, you’d want to weight further targets higher than closer ones.
Camera Angle to Target
This principle asks the question; which target am I actually looking at? We use a dot product result from two vectors (the camera’s forward vector & the unit direction between the player and the target) to determine how close the camera is looking at the target. We can then take this dot product float value and get the Acosine(degrees) to get the actual value in-degrees; if that matters for further calcuation or debugging text. Typically this camera angle is a strong indication of player intent and should be weighed appropriately in scoring calculations for picking a target.
Input Direction to Target
This principle asks an important question; in which direction is the player character moving, and how close is the angle between the targets’ relative position to the player and the input direction. In simpler terms, are we moving in the direction of the target? We again use a dot product result from two vectors (the normalized input direction and the unit direction between the player and the target) to determine how closely the input direction matches the direction to the target. This principle is very important when it comes to melee combat as its a strong indication of player intent if they are moving in the direction of a target that they would want to hit them with a sword.
Target Recency Bias
This principle poses the question; is this target more important due to recent actions from either the target itself, or the player? If you take melee combat as an example, when the player is button mashing and attacking a target, it can be assumed that they would want to continue to attack that same target. What you can do is store this target on each melee hit, or mark it with a Gameplay Tag, so that we can properly identify it when performing target selection, filtering, and scoring. What these biases can be, and how you mark them, are influenced by the type of game you are making, and the type of combat you want. However, it is an important aspect of targeting that should be considered.
Target Tag Query
This principle uses Unreal Engine terminology of ‘tagging’ to simply ask the question; is this target special in some way? This can be treated as a binary scoring measurement that can help make the difference in the heat of combat to ensure the best target is selected. When an enemy is about to attack, we can mark them with a Gameplay Tag, Enemy.IsAttacking
, and we can use target tag queries to score them a bit higher than enemies not attacking; the result leading to players’ choosing this enemy if two or more targets have similarly matching scores from other principles.
Target Visibility
This principle asks whether or not the target is visible to the player. Depending on the combat scenario, the target being on the screen may or may not matter; or more importantly, how recent was the target on-screen? Do we still consider a target valid if they have left the screen less than 3 seconds ago? There are other factors that can influence how important this aspect is for your combat; does your combat camera try to keep all targets on-screen at all times? Does AI design dictate that enemies will position themselves on-screen, or are there archetypes that challenge players from off-screen?
Gameplay Targeting System Plugin
The Gameplay Targeting System is a free plugin from Epic Games that gives alot of flexibility on how to select, filter, and sort targets. The system supports synchronous and asynchronous execution of target requests that return handles that can then be used to query results. The targeting system is uses three concepts to provide targets; Selection, Filtering, and Sorting. It is also important to state that the order in which these tasks happen does matter, and its crucial to perform these tasks in the order of selection, filtering, and then sorting. Let’s start with selection:
Target Selection
When we say Target Selection, we don’t mean the explicit selection of a specific target; we mean how we query the environment to return a list of potential targets that we can then filter and sort to return either a sorted list based on scoring, or just return the best target. By default, the target system provides a few different selection tasks, but we will talk about the AOE (Area of Effect) task, as shown below:
The task provides alot of different options to allow customization of how this environment query can look like; the shape (box, cylinder, sphere, or capsule), the size, position offsets, collision profiles, object types, and more.
Target Filters
Once you have the initial list of targets, we want to narrow this list down to only the important sub-set of targets based on our different filtering rules. By default, the target system provides one filter task that allows us to filter based on Actor Class:
We can use the Required Actor Class and Ignored Actor Class Filters to ensure we are only getting actors we truly care about. In the above example, we require the actor to be of class GASCourseNPC_Base while also actively ignoring actor classes of GASCoursePlayerCharacter; effectively only taking NPCs into consideration for further scoring and sorts. We may also want to filter enemies that are dead, or off-screen.
Target Sorting
Now that we have our final list of targets, its time to iterate over them and score them based on whatever criteria we want; this is where the true power of this system comes into play. For my project, I felt that having a sort class that scored targets based on their relative positions on-screen compared to the players’ input direction, and so I created such a class:
We will be diving more deeply into this class later on, so I won’t do that here. Essentially, we can have unique sorting classes that can take into consideration different gameplay elements to influence how each target is scored and sorted. Things like distance, camera direction; basically all the principles we listed earlier can be their own sort/scoring mechanism to check against.
Target Preset
Now that we know how we want to select, filter, and score our targets, we need a way to execute these chronologically so that we can get a properly sorted list, or the highest scoring target. This is where Target Presets come in; it is the target system’s way of knowing how to properly execute the aforementioned steps, and in which order. It is also within these presets that we define exactly how each step will execute; customizing parameters and changing execution order.
It is important to repeat that, although execution order may not matter entirely, it is key to understand how the pipeline works when gathering, filtering, and sorting our targets. For example, if we don’t first perform a selection task, we won’t have a list of targets to either filter or sort, making the whole preset useless.
Target Request Handles
The target system uses request handles to provide us with results of the target preset execution because the system provides both synchronous and asynchronous execution of target requests, and so these handles are a safe way to store access to our data. In Blueprints, we can use the Targeting Subsystem and the Targeting Request Handle to access both targeting results and targeting result actors. Depending on whether or not there is sorting involved with the targeting preset, the returned array may or may not be sorted.
If you are using the Gameplay Ability System like I am, there is an explicit ability task called Perform Targeting Request, that can be used to handle our targeting needs.
Custom Target Sorting Solutions
Although the Targeting System plugin from Epic Games handles a lot of stuff out of the box when it comes to targeting, it does not do everything.
UGASCourse_TargetSortBase
For my project, I created a base sorting class (UGASCourse_TargetSortBase) that I use as a base in which all my other sorting tasks extend from. The primary reason for this is due to how I wanted to handle scoring for my targeting.
Scoring
There are a few things that I wanted to do uniquely for my targeting that the default plugin doesn’t support out of the box:
Score Curves: I wanted to be able to define through float curves how scoring can change based on the input value from the scoring evaluation. For example, for the camera angle scoring, I wanted to allow the score to be 1.0f between 0 and 10 degrees, and then fall-off until the max camera angle of 30.0f.
Using float curves gives me the flexibility to adjust how scoring can work rather than relying on a simple clamped mapping range of values.
Additive Scoring: When first using the Target System plugin with my custom sort/scoring classes, I noticed that the score for each target was not properly being added when being passed in each scoring execution. For example, a target score of 1.1f from the camera angle scoring class was not being passed into the consecutive distance scoring class. Without additive scoring that persists through each scoring execution, I was unable to determine the highest scoring target to use. As a result, I had to override the void UGASCourse_TargetSortBase::Execute(const FTargetingRequestHandle& TargetingHandle) const
function of my UGASCourse_TargetSortBase class to achieve this:
void UGASCourse_TargetSortBase::Execute(const FTargetingRequestHandle& TargetingHandle) const
{
SetTaskAsyncState(TargetingHandle, ETargetingTaskAsyncState::Executing);if (TargetingHandle.IsValid())
{
if (FTargetingDefaultResultsSet* ResultData = FTargetingDefaultResultsSet::Find(TargetingHandle))
{
const int32 NumTargets = ResultData->TargetResults.Num();TArray<float> RawScores; for (const FTargetingDefaultResultData& TargetResult : ResultData->TargetResults) { const float RawScore = GetScoreForTarget(TargetingHandle, TargetResult); RawScores.Add(RawScore); } if(ensureMsgf(NumTargets == RawScores.Num(), TEXT("The cached raw scores should be the same size as the number of targets!"))) { // Adding the normalized scores to each target result. for (int32 TargetIterator = 0; TargetIterator < NumTargets; ++TargetIterator) { FTargetingDefaultResultData& TargetResult = ResultData->TargetResults[TargetIterator]; TargetResult.Score += RawScores[TargetIterator]; } } // sort the set ResultData->TargetResults.Sort([this](const FTargetingDefaultResultData& Lhs, const FTargetingDefaultResultData& Rhs) { return Lhs.Score < Rhs.Score; }); }
}
SetTaskAsyncState(TargetingHandle, ETargetingTaskAsyncState::Completed);
if(GASCourse_TargetingSystemCVars::bEnableSortTargetingDebug)
{
UE_LOG(LogTemp, Warning, TEXT(“Is Source Actor Moving? %s”), bIsSourceActorMoving ? TEXT(“TRUE”) : TEXT(“FALSE”));
UE_LOG(LogTemp, Warning, TEXT(“Final Score Multiplier = %f”), FinalScoreMultiplier);
}
}
Debugging
I wanted to emphasize the need for robust and clear debugging tools for each of my sorting classes, as it made developing and confirming their individual functionality a lot easier.
Using a combination of debug text, debug arrows, coloring, and debug boxes around each target I was able to make debugs that were clear. Each is controlled by a unique set of console variables to both enable debug drawing, and to disable the sorting functionality entirely, at runtime! You will see more when we break down each sorting class.
Here is the code of the UGASCourse_TargetSortBase
class:
GASCourse_TargetSortBase.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
include “Tasks/TargetingSortTask_Base.h”
include “GASCourse_TargetSortBase.generated.h”/**
- Lifecycle functions for sorting targets based on various criteria.
*/UCLASS(Blueprintable)
class GASCOURSE_API UGASCourse_TargetSortBase : public UTargetingSortTask_Base
{
GENERATED_BODY()protected:
/** Lifecycle function called when the task first begins */
virtual void Init(const FTargetingRequestHandle& TargetingHandle) const override;/** Evaluation function called by derived classes to process the targeting request */
virtual void Execute(const FTargetingRequestHandle& TargetingHandle) const override;/** Called on every target to get a Score for sorting. This score will be added to the Score float in FTargetingDefaultResultData */
virtual float GetScoreForTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const;/**
- @brief Mutable APawn pointer representing the source pawn for sorting targets.
- The SourcePawn variable is a mutable pointer to an APawn object that is initialized to nullptr.
- It is used to store the source pawn for sorting targets and can be modified.
/
UPROPERTY()
mutable APawn SourcePawn = nullptr;/**
- @brief Mutable pointer to a Player Controller representing the source player controller.
- The SourcePlayerController variable is a mutable pointer to an APlayerController object that is initialized to nullptr.
- It is used to store the source player controller for sorting targets and can be modified.
/
UPROPERTY()
mutable APlayerController SourcePlayerController = nullptr;/**
- Member variable to store the flag indicating whether the source actor is currently moving.
*/
UPROPERTY()
mutable bool bIsSourceActorMoving;/**
- Mutable variable to store the final score multiplier used in sorting targets
- in GASCourse_TargetSortBase class.
*/
UPROPERTY()
mutable float FinalScoreMultiplier = 1.0f;/**
- Mutable variable to store the score for sorting targets in the GASCourse_TargetSortBase class.
*/
UPROPERTY()
mutable float Score = 0.0f;public:
/**
- Boolean flag indicating whether the target is affected by the source actor movement input.
- If true, the target sorting may consider the source actor’s movement when calculating scores.
- By default, it is set to false.
*/
UPROPERTY(EditDefaultsOnly, Category = “GASCourse|Targeting|MovementInput”)
bool bAffectedBySourceActorMovement = true;/**
- @brief Score multiplier applied when source movement input is NOT detected
- Member variable representing the score multiplier applied when the source movement input is not detected.
- This value is used in sorting targets in the GASCourse_TargetSortBase class.
*/
UPROPERTY(EditDefaultsOnly, Category=“GASCourse|Targeting|Score”, meta=(ToolTip=“Score multiplier applied when source movement input is NOT detected”))
float DefaultScoreMultiplier = 1.0f;/**
- Score multiplier applied when source movement input is detected.
- This variable determines the multiplier to be applied to the score when movement input is detected
- for the source actor. It affects the sorting of targets in GASCourse. The score can be multiplied
- by this value when the source actor is moving.
*/
UPROPERTY(EditDefaultsOnly, Category=“GASCourse|Targeting|Score”, meta=(ToolTip=“Score multiplier applied when source movement input is detected”))
float ScoreMultiplierWhenMoving = 1.0f;/**
- Member variable to store a curve mapping float values to custom scores in GASCourse.
- If null, score calculations will be handled by class functionality.
/
UPROPERTY(EditDefaultsOnly, Category=“GASCourse|Targeting|Score”, meta=(ToolTip=“Custom score mapping based on a float curve value. If null, score calculations will be handled by class functionality.”))
UCurveFloat ScoreCurve = nullptr;/** Called on every target to get a Score for sorting. This score will be added to the Score float in FTargetingDefaultResultData */
UFUNCTION(BlueprintImplementableEvent, DisplayName=GetScoreForTarget, Category=Targeting)
float BP_GetScoreForTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const;};
GASCourse_TargetSortBase.cpp
// Fill out your copyright notice in the Description page of Project Settings.
include “Game/Systems/Targeting/Sort/GASCourse_TargetSortBase.h”
namespace GASCourse_TargetingSystemCVars
{
static bool bEnableSortTargetingDebug = false;
FAutoConsoleVariableRef CvarEnableSortingTargetingDebugging(
TEXT(“GASCourseDebug.Targeting.EnableDebug.Sort.Base”),
bEnableSortTargetingDebug,
TEXT(“Enable on-screen debugging for base sort targeting. (Enabled: true, Disabled: false)”));}
void UGASCourse_TargetSortBase::Init(const FTargetingRequestHandle& TargetingHandle) const
{
Super::Init(TargetingHandle);if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle))
{
if (APawn* InPawn = Cast(SourceContext->SourceActor))
{
SourcePawn = InPawn;
SourcePlayerController = Cast(SourcePawn->GetController());
bIsSourceActorMoving = SourcePawn->GetLastMovementInputVector().Length() > 0.0f;
}
}FinalScoreMultiplier = bIsSourceActorMoving ? ScoreMultiplierWhenMoving : DefaultScoreMultiplier;
}void UGASCourse_TargetSortBase::Execute(const FTargetingRequestHandle& TargetingHandle) const
{
SetTaskAsyncState(TargetingHandle, ETargetingTaskAsyncState::Executing);if (TargetingHandle.IsValid())
{
if (FTargetingDefaultResultsSet* ResultData = FTargetingDefaultResultsSet::Find(TargetingHandle))
{
const int32 NumTargets = ResultData->TargetResults.Num();TArray<float> RawScores; for (const FTargetingDefaultResultData& TargetResult : ResultData->TargetResults) { const float RawScore = GetScoreForTarget(TargetingHandle, TargetResult); RawScores.Add(RawScore); } if(ensureMsgf(NumTargets == RawScores.Num(), TEXT("The cached raw scores should be the same size as the number of targets!"))) { // Adding the normalized scores to each target result. for (int32 TargetIterator = 0; TargetIterator < NumTargets; ++TargetIterator) { FTargetingDefaultResultData& TargetResult = ResultData->TargetResults[TargetIterator]; TargetResult.Score += RawScores[TargetIterator]; } } // sort the set ResultData->TargetResults.Sort([this](const FTargetingDefaultResultData& Lhs, const FTargetingDefaultResultData& Rhs) { return Lhs.Score < Rhs.Score; }); }
}
SetTaskAsyncState(TargetingHandle, ETargetingTaskAsyncState::Completed);
if(GASCourse_TargetingSystemCVars::bEnableSortTargetingDebug)
{
UE_LOG(LogTemp, Warning, TEXT(“Is Source Actor Moving? %s”), bIsSourceActorMoving ? TEXT(“TRUE”) : TEXT(“FALSE”));
UE_LOG(LogTemp, Warning, TEXT(“Final Score Multiplier = %f”), FinalScoreMultiplier);
}
}float UGASCourse_TargetSortBase::GetScoreForTarget(const FTargetingRequestHandle& TargetingHandle,
const FTargetingDefaultResultData& TargetData) const
{
return Super::GetScoreForTarget(TargetingHandle, TargetData);
}
UGASCourse_TargetSortInputAngle
Below you will find how I used player input angle to score targets:

GASCourse_TargetSortInputAngle.h
/ Fill out your copyright notice in the Description page of Project Settings.
#pragma once
include “Game/Systems/Targeting/Sort/GASCourse_TargetSortBase.h”
include “GASCourse_TargetSortInputAngle.generated.h”/**
- UGASCourse_TargetSortInputAngle class is a subclass of UGASCourse_TargetSortBase
- that provides functionality for sorting targets based on input angle.
*/
UCLASS()
class GASCOURSE_API UGASCourse_TargetSortInputAngle : public UGASCourse_TargetSortBase
{
GENERATED_BODY()protected:
/** Lifecycle function called when the task first begins */
virtual void Init(const FTargetingRequestHandle& TargetingHandle) const override;/** Evaluation function called by derived classes to process the targeting request */
virtual void Execute(const FTargetingRequestHandle& TargetingHandle) const override;/** Called on every target to get a Score for sorting. This score will be added to the Score float in FTargetingDefaultResultData */
virtual float GetScoreForTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const;/**
- @brief Mutable FVector representing the input direction for sorting targets.
*/
UPROPERTY()
mutable FVector InputDirection;};
GASCourse_TargetSortInputAngle.cpp
// Fill out your copyright notice in the Description page of Project Settings.
include “Game/Systems/Targeting/Sort/GASCourse_TargetSortInputAngle.h”
include “Kismet/KismetMathLibrary.h”
include “Engine/LocalPlayer.h”
include “Kismet/KismetTextLibrary.h”namespace GASCourse_TargetingSystemCVars
{
static bool bEnableInputTargetingDebug = false;
FAutoConsoleVariableRef CvarEnableInputTargetingDebugging(
TEXT(“GASCourseDebug.Targeting.EnableDebug.Sort.InputAngle”),
bEnableInputTargetingDebug,
TEXT(“Enable on-screen debugging for input based targeting(Enabled: true, Disabled: false)”));static bool bDisableInputSortTargeting = false;
FAutoConsoleVariableRef CvarDisableInputSortTargeting(
TEXT(“GASCourseDebug.Targeting.Disable.Sort.InputAngle”),
bDisableInputSortTargeting,
TEXT(“Disable input based sorting for targeting.(Enabled: true, Disabled: false)”));};
void UGASCourse_TargetSortInputAngle::Init(const FTargetingRequestHandle& TargetingHandle) const
{
Super::Init(TargetingHandle);if(SourcePawn)
{
FRotator Delta = UKismetMathLibrary::NormalizedDeltaRotator(SourcePawn->GetActorRotation(), SourcePawn->GetControlRotation());
Delta.Pitch = 0.0f;
InputDirection = Delta.UnrotateVector(SourcePawn->GetLastMovementInputVector());#if !UE_BUILD_SHIPPING
if(GASCourse_TargetingSystemCVars::bEnableInputTargetingDebug)
{
FlushPersistentDebugLines(SourcePawn->GetWorld());
FlushDebugStrings(SourcePawn->GetWorld());
FVector LineEndInputDirection = SourcePawn->GetActorLocation() + InputDirection.GetSafeNormal() * 500.0f;
DrawDebugDirectionalArrow(SourcePawn->GetWorld(), SourcePawn->GetActorLocation(), LineEndInputDirection, 50.0f, FColor::Yellow, false, 5.0f, 0, 5.0f);
}
#endif
}}
void UGASCourse_TargetSortInputAngle::Execute(const FTargetingRequestHandle& TargetingHandle) const
{
Super::Execute(TargetingHandle);
}float UGASCourse_TargetSortInputAngle::GetScoreForTarget(const FTargetingRequestHandle& TargetingHandle,
const FTargetingDefaultResultData& TargetData) const
{
if(GASCourse_TargetingSystemCVars::bDisableInputSortTargeting)
{
return 0.0f;
}if(SourcePawn && SourcePlayerController)
{
if(TargetData.HitResult.HasValidHitObjectHandle())
{
if(AActor* HitActor = TargetData.HitResult.HitObjectHandle.FetchActor())
{
FRotator Delta = UKismetMathLibrary::NormalizedDeltaRotator(SourcePawn->GetActorRotation(), SourcePawn->GetControlRotation());
Delta.Pitch = 0.0f;
InputDirection = Delta.UnrotateVector(SourcePawn->GetLastMovementInputVector());FVector UnitDirection = UKismetMathLibrary::GetDirectionUnitVector(SourcePawn->GetActorLocation(), HitActor->GetActorLocation()); float InputToTargetDotProduct = UKismetMathLibrary::Dot_VectorVector(InputDirection.GetSafeNormal(), UnitDirection); Score = ScoreCurve ? ScoreCurve->GetFloatValue(InputToTargetDotProduct) : InputToTargetDotProduct;
#if !UE_BUILD_SHIPPING
if(GASCourse_TargetingSystemCVars::bEnableInputTargetingDebug)
{
UE_LOG(LogTemp, Warning, TEXT(“CURRENT SCORE: %f”), TargetData.Score);
UE_LOG(LogTemp, Warning, TEXT(“Input ANGLE = %f”), UKismetMathLibrary::DegAcos(InputToTargetDotProduct));
FString ScoreString = UKismetTextLibrary::Conv_DoubleToText(Score, HalfToEven, false, false, 1, 324,
1, 2).ToString();
FString FinalScoreString = UKismetTextLibrary::Conv_DoubleToText(Score * FinalScoreMultiplier, HalfToEven, false, false, 1, 324,
1, 2).ToString();
FString InputAngleString = UKismetTextLibrary::Conv_DoubleToText(UKismetMathLibrary::DegAcos(InputToTargetDotProduct), HalfToEven, false, false, 1, 324,
1, 2).ToString();
FString EndResults = *FString::Printf(TEXT(“Score = %s | Final Score = %s | Angle = %s°” ), *ScoreString, *FinalScoreString, *InputAngleString);FLinearColor ScoreColor = UKismetMathLibrary::LinearColorLerp(FLinearColor::Red, FLinearColor::Green, Score); FVector ActorOrigin(0.0f); FVector ActorExtents(0.0f); HitActor->GetActorBounds(true, ActorOrigin, ActorExtents, false); DrawDebugBox(HitActor->GetWorld(), HitActor->GetActorLocation(), ActorExtents, ScoreColor.ToFColor(true), false, 5.0f, 0, 2.0f); DrawDebugString(HitActor->GetWorld(), FVector(-ActorExtents.X, -125.0f, ActorExtents.Z), EndResults, HitActor, ScoreColor.ToFColor(true), 5.0f, true, 1.0f); UE_LOG(LogTemp, Warning, TEXT("Score = %f /n Final Score = %f"), Score, Score * FinalScoreMultiplier); FVector LineEndUnitDirection = SourcePawn->GetActorLocation() + UnitDirection * 500.0f; DrawDebugDirectionalArrow(SourcePawn->GetWorld(), SourcePawn->GetActorLocation(), LineEndUnitDirection, 50.0f, FColor::Red, false, 5.0f, 0, 5.0f); }
#endif
return Score * FinalScoreMultiplier;
}
}
}
return 0.0f;
}
UGASCourse_TargetSortCameraAngle
Below you will find how I used player camera angle to score targets:

GASCourse_TargetSortCameraAngle.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
include “Game/Systems/Targeting/Sort/GASCourse_TargetSortBase.h”
include “GASCourse_TargetSortCameraAngle.generated.h”/**
- Class representing a camera angle-based target sorting task within the GASCourse module.
- This class inherits from UGASCourse_TargetSortBase.
*/
UCLASS(Blueprintable)
class GASCOURSE_API UGASCourse_TargetSortCameraAngle : public UGASCourse_TargetSortBase
{
GENERATED_BODY()public:
/**
- Represents the maximum angle that the camera can be tilted when targeting in the GASCourse module.
- This variable is used to limit the angle of the camera when calculating scores based on camera angles.
*/
UPROPERTY(EditDefaultsOnly, Category = “GASCourse|Targeting|Camera”)
float MaxCameraAngle = 30.0f;/** Lifecycle function called when the task first begins */
virtual void Init(const FTargetingRequestHandle& TargetingHandle) const override;/** Evaluation function called by derived classes to process the targeting request */
virtual void Execute(const FTargetingRequestHandle& TargetingHandle) const override;/** Called on every target to get a Score for sorting. This score will be added to the Score float in FTargetingDefaultResultData */
virtual float GetScoreForTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const;protected:
/**
- Represents a mutable pointer to an instance of APlayerCameraManager.
- This variable is used to manage the player camera and retrieve camera-related information
- needed for targeting and scoring calculations within the GASCourse module.
/
UPROPERTY()
mutable APlayerCameraManager CameraManager = nullptr;};
GASCourse_TargetSortCameraAngle.cpp
// Fill out your copyright notice in the Description page of Project Settings.
include “Game/Systems/Targeting/Sort/GASCourse_TargetSortCameraAngle.h”
include “Kismet/KismetMathLibrary.h”
include “Kismet/KismetTextLibrary.h”namespace GASCourse_TargetingSystemCVars
{
static bool bEnableCameraTargetingDebug = false;
FAutoConsoleVariableRef CvarEnableCameraTargetingDebugging(
TEXT(“GASCourseDebug.Targeting.EnableDebug.Sort.CameraAngle”),
bEnableCameraTargetingDebug,
TEXT(“Enable on-screen debugging for camera based targeting. (Enabled: true, Disabled: false)”));static bool bDisableCameraSortTargeting = false;
FAutoConsoleVariableRef CvarDisableCameraSortTargeting(
TEXT(“GASCourseDebug.Targeting.Disable.Sort.CameraAngle”),
bDisableCameraSortTargeting,
TEXT(“Disable camera based sorting for targeting.(Enabled: true, Disabled: false)”));}
void UGASCourse_TargetSortCameraAngle::Init(const FTargetingRequestHandle& TargetingHandle) const
{
Super::Init(TargetingHandle);if(GASCourse_TargetingSystemCVars::bDisableCameraSortTargeting)
{
return;
}if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle))
{
if (APawn* InstigatorPawn = Cast(SourceContext->SourceActor))
{
if(APlayerController* PlayerController = Cast(InstigatorPawn->GetController()))
{
CameraManager = PlayerController->PlayerCameraManager;
}#if !UE_BUILD_SHIPPING
if(GASCourse_TargetingSystemCVars::bEnableCameraTargetingDebug)
{
FlushPersistentDebugLines(SourcePawn->GetWorld());
FlushDebugStrings(SourcePawn->GetWorld());
FRotator ControlRotation = SourcePlayerController->GetControlRotation();
ControlRotation.Pitch = 0.0f;
FVector LineEndControlRotation = SourcePawn->GetActorLocation() + UKismetMathLibrary::Conv_RotatorToVector(ControlRotation) * 500.0f;
DrawDebugDirectionalArrow(SourcePawn->GetWorld(), SourcePawn->GetActorLocation(), LineEndControlRotation, 50.0f, FColor::Green, false, 5.0f, 0, 5.0f);
}
#endif
}
}
}void UGASCourse_TargetSortCameraAngle::Execute(const FTargetingRequestHandle& TargetingHandle) const
{
Super::Execute(TargetingHandle);
}float UGASCourse_TargetSortCameraAngle::GetScoreForTarget(const FTargetingRequestHandle& TargetingHandle,
const FTargetingDefaultResultData& TargetData) const
{
if((GASCourse_TargetingSystemCVars::bDisableCameraSortTargeting))
{
return 0.0f;
}if(CameraManager)
{
if(TargetData.HitResult.HasValidHitObjectHandle())
{
if(AActor* HitActor = TargetData.HitResult.HitObjectHandle.FetchActor())
{
FVector CameraLocation = CameraManager->GetCameraLocation();
FVector CameraForwardVector = CameraManager->GetActorForwardVector();
FVector UnitDirection = UKismetMathLibrary::GetDirectionUnitVector(SourcePawn->GetActorLocation(), HitActor->GetActorLocation());float CameraToTargetDotProduct = UKismetMathLibrary::Dot_VectorVector(CameraForwardVector, UnitDirection); float DotToDegrees = UKismetMathLibrary::DegAcos(CameraToTargetDotProduct); Score = ScoreCurve ? ScoreCurve->GetFloatValue(DotToDegrees) : UKismetMathLibrary::MapRangeClamped(DotToDegrees, 0.0f, MaxCameraAngle, 1.0f, 0.0f);
#if !UE_BUILD_SHIPPING
if(GASCourse_TargetingSystemCVars::bEnableCameraTargetingDebug)
{
UE_LOG(LogTemp, Warning, TEXT(“CURRENT SCORE: %f”), TargetData.Score);
UE_LOG(LogTemp, Warning, TEXT(“Camera ANGLE = %f”), DotToDegrees);
FString ScoreString = UKismetTextLibrary::Conv_DoubleToText(Score, HalfToEven, false, false, 1, 324,
1, 2).ToString();
FString FinalScoreString = UKismetTextLibrary::Conv_DoubleToText(Score * FinalScoreMultiplier, HalfToEven, false, false, 1, 324,
1, 2).ToString();
FString CameraAngleString = UKismetTextLibrary::Conv_DoubleToText(DotToDegrees, HalfToEven, false, false, 1, 324,
1, 2).ToString();
FString EndResults = *FString::Printf(TEXT(“Score = %s | Final Score = %s | Angle = %s°” ), *ScoreString, *FinalScoreString, *CameraAngleString);FLinearColor ScoreColor = UKismetMathLibrary::LinearColorLerp(FLinearColor::Red, FLinearColor::Green, Score); FVector ActorOrigin(0.0f); FVector ActorExtents(0.0f); HitActor->GetActorBounds(true, ActorOrigin, ActorExtents, false); DrawDebugBox(HitActor->GetWorld(), HitActor->GetActorLocation(), ActorExtents, ScoreColor.ToFColor(true), false, 5.0f, 0, 2.0f); DrawDebugString(HitActor->GetWorld(), FVector(-ActorExtents.X, -125.0f, ActorExtents.Z), EndResults, HitActor, ScoreColor.ToFColor(true), 5.0f, true, 1.0f); FVector LineEndUnitDirection = SourcePawn->GetActorLocation() + UnitDirection * 500.0f; DrawDebugDirectionalArrow(SourcePawn->GetWorld(), SourcePawn->GetActorLocation(), LineEndUnitDirection, 50.0f, FColor::Red, false, 5.0f, 0, 5.0f); }
#endif
return Score * FinalScoreMultiplier;
}
}
}
return 0.0f;
}
UGASCourse_TargetSortDistance
Below you will find how I used distance to score targets:

GASCourse_TargetSortDistance.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
include “Game/Systems/Targeting/Sort/GASCourse_TargetSortBase.h”
include “GASCourse_TargetSortDistance.generated.h”/**
- @class UGASCourse_TargetSortDistance
- @brief A class that represents a target sorting task based on distance in GASCourse.
- UGASCourse_TargetSortDistance is derived from UGASCourse_TargetSortBase and provides functionality for sorting
- targets based on distance.
*/
UCLASS()
class GASCOURSE_API UGASCourse_TargetSortDistance : public UGASCourse_TargetSortBase
{
GENERATED_BODY()public:
/**
- @brief Editable default maximum distance for targeting in GASCourse.
- Represents the maximum distance in float units within which targets can be considered for sorting in the GASCourse_TargetSortDistance class.
- The default value is set to 600.0f and can be edited in the GASCourse editor. This parameter is applicable when target sorting based on distance is enabled and not disabled.
- @see UGASCourse_TargetSortDistance
*/
UPROPERTY(EditDefaultsOnly, Category = “GASCourse|Targeting|Distance”)
float MaxDistance = 600.0f;protected:
/** Lifecycle function called when the task first begins */
virtual void Init(const FTargetingRequestHandle& TargetingHandle) const override;/** Evaluation function called by derived classes to process the targeting request */
virtual void Execute(const FTargetingRequestHandle& TargetingHandle) const override;/** Called on every target to get a Score for sorting. This score will be added to the Score float in FTargetingDefaultResultData */
virtual float GetScoreForTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const;};
GASCourse_TargetSortDistance.cpp
// Fill out your copyright notice in the Description page of Project Settings.
include “Game/Systems/Targeting/Sort/GASCourse_TargetSortDistance.h”
include “Kismet/KismetMathLibrary.h”
include “Kismet/KismetTextLibrary.h”namespace GASCourse_TargetingSystemCVars
{
static bool bEnableDistanceTargetingDebug = false;
FAutoConsoleVariableRef CvarEnableDistanceTargetingDebugging(
TEXT(“GASCourseDebug.Targeting.EnableDebug.Sort.Distance”),
bEnableDistanceTargetingDebug,
TEXT(“Enable on-screen debugging for distance based targeting. (Enabled: true, Disabled: false)”));static bool bDisableDistanceSortTargeting = false;
FAutoConsoleVariableRef CvarDisableDistanceSortTargeting(
TEXT(“GASCourseDebug.Targeting.Disable.Sort.Distance”),
bDisableDistanceSortTargeting,
TEXT(“Disable distance based sorting for targeting.(Enabled: true, Disabled: false)”));}
void UGASCourse_TargetSortDistance::Init(const FTargetingRequestHandle& TargetingHandle) const
{
Super::Init(TargetingHandle);if(GASCourse_TargetingSystemCVars::bDisableDistanceSortTargeting)
{
return;
}#if !UE_BUILD_SHIPPING
if(GASCourse_TargetingSystemCVars::bEnableDistanceTargetingDebug)
{
FlushPersistentDebugLines(SourcePawn->GetWorld());
FlushDebugStrings(SourcePawn->GetWorld());
}
#endif}
void UGASCourse_TargetSortDistance::Execute(const FTargetingRequestHandle& TargetingHandle) const
{
Super::Execute(TargetingHandle);
}float UGASCourse_TargetSortDistance::GetScoreForTarget(const FTargetingRequestHandle& TargetingHandle,
const FTargetingDefaultResultData& TargetData) const
{
if(GASCourse_TargetingSystemCVars::bDisableDistanceSortTargeting)
{
return 0.0f;
}if(SourcePawn)
{
if(TargetData.HitResult.HasValidHitObjectHandle())
{
if(AActor* HitActor = TargetData.HitResult.HitObjectHandle.FetchActor())
{
float DistanceToTarget = UKismetMathLibrary::Vector_Distance(SourcePawn->GetActorLocation(), HitActor->GetActorLocation());
Score = ScoreCurve ? ScoreCurve->GetFloatValue(DistanceToTarget) : UKismetMathLibrary::MapRangeClamped(DistanceToTarget, 0.0f, MaxDistance, 1.0f, 0.0f);#if !UE_BUILD_SHIPPING
if(GASCourse_TargetingSystemCVars::bEnableDistanceTargetingDebug)
{
UE_LOG(LogTemp, Warning, TEXT(“CURRENT SCORE: %f”), TargetData.Score);
FVector UnitDirection = UKismetMathLibrary::GetDirectionUnitVector(SourcePawn->GetActorLocation(), HitActor->GetActorLocation());
UE_LOG(LogTemp, Warning, TEXT(“Distance to %s = %f”), *HitActor->GetName(), DistanceToTarget);
FString ScoreString = UKismetTextLibrary::Conv_DoubleToText(Score, HalfToEven, false, false, 1, 324,
1, 2).ToString();
FString FinalScoreString = UKismetTextLibrary::Conv_DoubleToText(Score * FinalScoreMultiplier, HalfToEven, false, false, 1, 324,
1, 2).ToString();
FString DistanceString = UKismetTextLibrary::Conv_DoubleToText(DistanceToTarget, HalfToEven, false, false, 1, 324,
1, 2).ToString();
FString EndResults = *FString::Printf(TEXT(“Score = %s | Final Score = %s | Distance = %s” ), *ScoreString, *FinalScoreString, *DistanceString);FLinearColor ScoreColor = UKismetMathLibrary::LinearColorLerp(FLinearColor::Red, FLinearColor::Green, Score); FVector ActorOrigin(0.0f); FVector ActorExtents(0.0f); HitActor->GetActorBounds(true, ActorOrigin, ActorExtents, false); DrawDebugBox(HitActor->GetWorld(), HitActor->GetActorLocation(), ActorExtents, ScoreColor.ToFColor(true), false, 5.0f, 0, 2.0f); DrawDebugString(HitActor->GetWorld(), FVector(-ActorExtents.X, -125.0f, ActorExtents.Z), EndResults, HitActor, ScoreColor.ToFColor(true), 5.0f, true, 1.0f); UE_LOG(LogTemp, Warning, TEXT("Score = %f /n Final Score = %f"), Score, Score * FinalScoreMultiplier); FVector LineEndUnitDirection = SourcePawn->GetActorLocation() + UnitDirection * DistanceToTarget; FVector LineEndMaxDistanceDirection = SourcePawn->GetActorLocation() + UnitDirection * MaxDistance; DrawDebugDirectionalArrow(SourcePawn->GetWorld(), SourcePawn->GetActorLocation(), LineEndUnitDirection, 50.0f, ScoreColor.ToFColor(true), false, 5.0f, 0, 5.0f); DrawDebugDirectionalArrow(SourcePawn->GetWorld(), SourcePawn->GetActorLocation() + FVector(0.0f, 0.0f, 10.0f), LineEndMaxDistanceDirection, 50.0f, FColor::Yellow, false, 5.0f, 0, 5.0f); }
#endif
return Score * FinalScoreMultiplier;
}
}
}
return 0.0f;
}
Custom Target Filter Solution | BP_TargetFilter_MatchesTagQuery
As mentioned previously, the use of Gameplay Tags can be powerful for labeling our targets when in specific states so that we can use these labels as markup for scoring them or filtering them. I decided to create this in Blueprints as the logic itself is not overly complicated and so I do not believe its worth any sort of performance gain to move it over to C++.
I use the BP_TargetFilter_MatchesTagQuery to check if a target is dead using the Status.Death
tag. This allows me to ensure that dead enemies will not be targeted, even during the brief few seconds between dying and being removed from the world.
The last thing that I did was add the following code into the TargetingSubsystem.cpp to allow me to properly debug draw the final results of the executed target preset:
TargetingSubsystem.cpp
static bool bDebugDrawFinalTargetScoring = false;
FAutoConsoleVariableRef CvarDebugDrawFinalTargetinScoring(
TEXT(“GASCourseDebug.Targeting.EnableDebug.ShowFinalScores”),
bDebugDrawFinalTargetScoring,
TEXT(“Toggles if we want to visualize the final scoring of the targeting handle (Enabled: true, Disabled: false)”)void UTargetingSubsystem::ExecuteTargetingRequestWithHandleInternal(FTargetingRequestHandle TargetingHandle, FTargetingRequestDelegate CompletionDelegate, FTargetingRequestDynamicDelegate CompletionDynamicDelegate)
if(TargetingSystemCVars::bDebugDrawFinalTargetScoring)
{
if (const FTargetingDefaultResultsSet* ResultSet = FTargetingDefaultResultsSet::Find(TargetingHandle))
{
if (const FTargetingSourceContext* SourceContext = FTargetingSourceContext::Find(TargetingHandle))
{
if (APawn* InPawn = Cast(SourceContext->SourceActor))
{
FlushPersistentDebugLines(InPawn->GetWorld());
FlushDebugStrings(InPawn->GetWorld());
}
}float HighScore = 0.0f; for(FTargetingDefaultResultData Result : ResultSet->TargetResults) { if(Result.Score >= HighScore) { HighScore = Result.Score; } } for(FTargetingDefaultResultData Result : ResultSet->TargetResults) { AActor* HitActor = Result.HitResult.GetActor(); FVector ActorOrigin(0.0f); FVector ActorExtents(0.0f); FLinearColor ScoreColor = UKismetMathLibrary::LinearColorLerp(FLinearColor::Red, FLinearColor::Green, UKismetMathLibrary::SafeDivide(Result.Score, HighScore)); FString ScoreString = *FString::Printf(TEXT("Final Score = %f" ), Result.Score); HitActor->GetActorBounds(true, ActorOrigin, ActorExtents, false); DrawDebugBox(HitActor->GetWorld(), HitActor->GetActorLocation(), ActorExtents, ScoreColor.ToFColor(true), false, 5.0f, 0, 2.0f); DrawDebugString(HitActor->GetWorld(), FVector(-ActorExtents.X, -125.0f, ActorExtents.Z), ScoreString, HitActor, ScoreColor.ToFColor(true), 5.0f, true, 1.0f); } } }
Other Concepts to Consider
We have written about how I use the targeting system for my project, but there are other considerations to take into account that will most likely come up in my project, but also be present within your combat:
Bone Selection
Assuming your targets are characters with skeletal meshes, you may want to consider different bones/sockets of the character in your targeting selection logic. If your game supports limb targeting, or you have large monsters, you could look into how to label these bones so that you can extend target presets to take them into account within a singular target.
Target Locking
A very common mechanic in action games is the use of target locking, or hard-locking, which means that the player explicitly wants to target a singular enemy and keep this target until the target dies, or the player manually removes the lock. How can we extend the target system to take into account target locking? How can we use target presets to query the right target for the player to lock onto? How do we handle target switching while in target lock mode? Can players switch between targets using the thumb-stick, or other input?
References:
Thank you for taking the time to read this post, and I hope you were able to take something away from it. Please let me know if I got any information wrong, or explained something incorrectly! Also add any thoughts or questions or code review feedback so we can all learn together
Next Blog Post Topic:
Input Movement Interruption