Hello everyone!
My name is Devin Sherry and I am a Senior Gameplay Designer at CD Projekt Red working on the next generation Witcher series. I’ve also worked at People Can Fly on the Outriders and Outriders: Worldslayer expansion titles, in addition to co-authoring a few textbooks about Unreal Engine 4 & 5. Lastly, I have a history of making YouTube tutorials about Blueprint/C++ related topics, which will be linked below; however, I haven’t had the time to focus on this aspect of my career. But enough about me, let’s talk about the purpose of this discussion:
I am working on a side project that I am calling the GAS Course Project, a small game example built from scratch using the Gameplay Ability System plugin in my efforts to continue my own learning and game development journey, but to also hopefully help others learn along the way. I have been working on this project for about an hour a day, a few times a week starting from the beginning of 2023, and I am nowhere near being finished however, I wanted to start a forum discussion regarding the project, in a form of blog posts, talking about what I am working on, gathering feedback, showing bits of code and game features in hopes to help others, but to also help myself improve. My goal, by the end of the project, is to have an example project available for free on the Unreal Engine marketplace for everyone to use as a base, or working example, of features related to the Gameplay Ability System plugin. In addition, I want to provide robust documentation, both in the forms of these forum posts but also documentation similarly formatted as Tranek’s GAS Documentation - Big shout out to Tranek because their documentation is priceless. Ideally I would then make a tutorial course series breaking everything down and remaking things from scratch; whether or not this course would be free, let alone exist in the first place, is still unknown.
This initial entry in the discussion is to introduce everyone to both myself, and to the project and show-case one game feature as a kick-off point. To finish introductions, here are my socials and links to short videos (both on Tiktok and Youtube) showcasing some parts of the project. Also, I try to stream my work Monday, Tuesday, Thursday at around 7pm (GMT+1) if anyone is interested, a follow there would be appreciated, and I also have a Discord server for those interested in joining.
Twitch : Twitch
Discord : UE5 - GAS Course Project - JevinsCherries
Tiktok : Devin Sherry (@jevinscherriesgamedev) | TikTok
Youtube : https://youtube.com/@jevinscherries?si=GKt32rDN6z2wn5xV
Twitter : https://twitter.com/jevincherries
Instagram : Jevins Cherries Gamedev (@jevinscherriesgamedev) • Instagram photos and videos
Now, let’s talk about something that I hope will interest you, Wait Target Data:
What is Wait Target Data
Classes to research on your own:
AGameplayAbilityTargetActor
UAbilityTask_WaitTargetData
AGameplayAbilityWorldReticle
Wait Target Data is a latent ability task that allows users to obtain Target Data using any form of overlap detection needed to do so. The task allows for (EGameplayTargetingConfirmation) Instant, User Confirmed, Custom and Custom Multi returning of the data; however, my experience is only with Instant and User Confirmed methods. You can find more information about them in GameplayAbilityTargetTypes.h
UENUM(BlueprintType)
namespace EGameplayTargetingConfirmation
{
/** Describes how the targeting information is confirmed */
enum Type : int
{
/** The targeting happens instantly without special logic or user input deciding when to 'fire' */
Instant,
/** The targeting happens when the user confirms the targeting */
UserConfirmed,
/** The GameplayTargeting Ability is responsible for deciding when the targeting data is ready. Not supported by all TargetingActors */
Custom,
/** The GameplayTargeting Ability is responsible for deciding when the targeting data is ready. Not supported by all TargetingActors. Should not destroy upon data production */
CustomMulti,
};
}
For instant:
When using the Confirmation Type Instant, the task will automatically call the method:
/** Outside code is saying 'stop and just give me what you have.' Returns true if the ability accepts this and can be forgotten. */
UFUNCTION()
virtual void ConfirmTargeting();
This will return the target data immediately, depending on your implementation of the function, here is what I do:
void AGASCourseTargetActor_CameraTrace::ConfirmTargeting()
{
check(ShouldProduceTargetData());
if (SourceActor)
{
const FVector Origin = PerformTrace(SourceActor).Location;
FGameplayAbilityTargetDataHandle Handle = MakeTargetData(PerformOverlap(Origin), Origin);
TargetDataReadyDelegate.Broadcast(Handle);
}
}
NOTE: I don’t use the Instant confirmation type for my aim cast ability, and instead use User Confirmed!
For User Confirmed:
In order to setup the input needed for User Confirmed, you need to do the following when setting up the Input Configuration:
if(UGASCourseAbilitySystemComponent* MyASC = CastChecked<UGASCourseAbilitySystemComponent>( GetAbilitySystemComponent()))
{
EnhancedInputComponent->BindActionByTag(InputConfig, InputTag_ConfirmTargetData, ETriggerEvent::Triggered, MyASC, &UGASCourseAbilitySystemComponent::LocalInputConfirm);
EnhancedInputComponent->BindActionByTag(InputConfig, InputTag_CancelTargetData, ETriggerEvent::Triggered, MyASC, &UGASCourseAbilitySystemComponent::LocalInputCancel);
}
The assumption here for those following is that you are using Enhanced Input to bind ability activation to keyboard/mouse/gamepad button presses. Don’t worry, my next blog post will be about how to set this up!
The base class GameplayAbilityTargetActor has the method:
void AGameplayAbilityTargetActor::BindToConfirmCancelInputs()
and this method is reponsible for finding and adding those call-backs shown above.
When confirming or cancelling the Wait Target Data task, the following functions are called respectively:
void AGASCourseTargetActor_CameraTrace::ConfirmTargetingAndContinue()
{
check(ShouldProduceTargetData());
if (SourceActor)
{
const FVector Origin = PerformTrace(SourceActor).Location;
FGameplayAbilityTargetDataHandle Handle = MakeTargetData(PerformOverlap(Origin), Origin);
TargetDataReadyDelegate.Broadcast(Handle);
}
}
void AGASCourseTargetActor_CameraTrace::CancelTargeting()
{
Super::CancelTargeting();
}
The Ability Task is more or less a wrapper to the behavior implemented in the chosen GameplayAbilityTargetActor class; by default there a few options:
GameplayAbilityTargetActor_ActorPlacement
Allows to spawn a preview actor, using the trace data hit result used from the PerformTrace() method:
//Might want to override this function to allow for a radius check against the ground, possibly including a height check. Or might want to do it in ground trace.
//FHitResult AGameplayAbilityTargetActor_ActorPlacement::PerformTrace(AActor* InSourceActor) const
GameplayAbilityTargetActor_GroundTrace
This is meant to trace for and detect the ground in front of the player, within X unit Range. The trace is performed via the method FHitResult AGameplayAbilityTargetActor_GroundTrace::PerformTrace(AActor* InSourceActor)
and takes advantage of the helper method void AGameplayAbilityTargetActor_Trace::AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params, const FVector& TraceStart, FVector& OutTraceEnd, bool bIgnorePitch) const
FHitResult AGameplayAbilityTargetActor_GroundTrace::PerformTrace(AActor* InSourceActor)
{
bool bTraceComplex = false;
FCollisionQueryParams Params(SCENE_QUERY_STAT(AGameplayAbilityTargetActor_GroundTrace), bTraceComplex);
Params.bReturnPhysicalMaterial = true;
Params.AddIgnoredActor(InSourceActor);
FVector TraceStart = StartLocation.GetTargetingTransform().GetLocation();// InSourceActor->GetActorLocation();
FVector TraceEnd;
AimWithPlayerController(InSourceActor, Params, TraceStart, TraceEnd); //Effective on server and launching client only
// ------------------------------------------------------
FHitResult ReturnHitResult;
//Use a line trace initially to see where the player is actually pointing
LineTraceWithFilter(ReturnHitResult, InSourceActor->GetWorld(), Filter, TraceStart, TraceEnd, TraceProfile.Name, Params);
//Default to end of trace line if we don't hit anything.
if (!ReturnHitResult.bBlockingHit)
{
ReturnHitResult.Location = TraceEnd;
}
//Second trace, straight down. Consider using InSourceActor->GetWorld()->NavigationSystem->ProjectPointToNavigation() instead of just going straight down in the case of movement abilities (flag/bool).
TraceStart = ReturnHitResult.Location - (TraceEnd - TraceStart).GetSafeNormal(); //Pull back very slightly to avoid scraping down walls
TraceEnd = TraceStart;
TraceStart.Z += CollisionHeightOffset;
TraceEnd.Z -= 99999.0f;
LineTraceWithFilter(ReturnHitResult, InSourceActor->GetWorld(), Filter, TraceStart, TraceEnd, TraceProfile.Name, Params);
//if (!ReturnHitResult.bBlockingHit) then our endpoint may be off the map. Hopefully this is only possible in debug maps.
bLastTraceWasGood = true; //So far, we're good. If we need a ground spot and can't find one, we'll come back.
//Use collision shape to find a valid ground spot, if appropriate
if (CollisionShape.ShapeType != ECollisionShape::Line)
{
ReturnHitResult.Location.Z += CollisionHeightOffset; //Rise up out of the ground
TraceStart = InSourceActor->GetActorLocation();
TraceEnd = ReturnHitResult.Location;
TraceStart.Z += CollisionHeightOffset;
bLastTraceWasGood = AdjustCollisionResultForShape(TraceStart, TraceEnd, Params, ReturnHitResult);
if (bLastTraceWasGood)
{
ReturnHitResult.Location.Z -= CollisionHeightOffset; //Undo the artificial height adjustment
}
}
if (AGameplayAbilityWorldReticle* LocalReticleActor = ReticleActor.Get())
{
LocalReticleActor->SetIsTargetValid(bLastTraceWasGood);
LocalReticleActor->SetActorLocation(ReturnHitResult.Location);
}
// Reset the trace start so the target data uses the correct origin
ReturnHitResult.TraceStart = StartLocation.GetTargetingTransform().GetLocation();
return ReturnHitResult;
}
GameplayAbilityTargetActor_Radius
This is meant to perform a sphere overlap trace around the ability owner, or whichever data you place in the Start Location parameter of the task; most times you would use the method MakeTargetDataInfoFromOwnerActor().
GameplayAbilityTargetActor_SingleLine
This is meant to perform a single line trace using vectors calculated from the method void AGameplayAbilityTargetActor_Trace::AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params, const FVector& TraceStart, FVector& OutTraceEnd, bool bIgnorePitch) const
Lyra uses similar mechanisms when performing weapon traces.
NOTE: This works well for first/third person perspectives, but with top-down and isometric viewpoints, custom solutions need to be made (at least from my experience when developing my custom GameplayAbilityTargetActor classes)
void AGameplayAbilityTargetActor_Trace::AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params, const FVector& TraceStart, FVector& OutTraceEnd, bool bIgnorePitch) const
{
if (!OwningAbility) // Server and launching client only
{
return;
}
APlayerController* PC = OwningAbility->GetCurrentActorInfo()->PlayerController.Get();
check(PC);
FVector ViewStart;
FRotator ViewRot;
PC->GetPlayerViewPoint(ViewStart, ViewRot);
const FVector ViewDir = ViewRot.Vector();
FVector ViewEnd = ViewStart + (ViewDir * MaxRange);
ClipCameraRayToAbilityRange(ViewStart, ViewDir, TraceStart, MaxRange, ViewEnd);
FHitResult HitResult;
LineTraceWithFilter(HitResult, InSourceActor->GetWorld(), Filter, ViewStart, ViewEnd, TraceProfile.Name, Params);
const bool bUseTraceResult = HitResult.bBlockingHit && (FVector::DistSquared(TraceStart, HitResult.Location) <= (MaxRange * MaxRange));
const FVector AdjustedEnd = (bUseTraceResult) ? HitResult.Location : ViewEnd;
FVector AdjustedAimDir = (AdjustedEnd - TraceStart).GetSafeNormal();
if (AdjustedAimDir.IsZero())
{
AdjustedAimDir = ViewDir;
}
if (!bTraceAffectsAimPitch && bUseTraceResult)
{
FVector OriginalAimDir = (ViewEnd - TraceStart).GetSafeNormal();
if (!OriginalAimDir.IsZero())
{
// Convert to angles and use original pitch
const FRotator OriginalAimRot = OriginalAimDir.Rotation();
FRotator AdjustedAimRot = AdjustedAimDir.Rotation();
AdjustedAimRot.Pitch = OriginalAimRot.Pitch;
AdjustedAimDir = AdjustedAimRot.Vector();
}
}
OutTraceEnd = TraceStart + (AdjustedAimDir * MaxRange);
}
Now that you have the target data, what can you do with it?
There are alot of helper functions that you can utilize from the returned Gameplay Ability Target Data Handle Structure of the Wait Target Data task; here are just a few:
Target Data Has Hit Result
Returns whether or not the target data has a valid hit result.
Get All Actors from Target Data
Returns the array of actors found from the Wait Target Data task. This is dependent on how you handle filtering:
Get Hit Result from Target Data
Returns the hit result, if one exists, of the wait target data. This can be useful for single line traces for impacts of weapons, for example.
Does Target Data Contain Actor
Returns whether or not the target data contains a specific actor; can be useful if you need to do something special in conditions of capturing a specific actor.
Apply Gameplay Effect To Target
Allows you to apply a gameplay effect to all actors found in the target data; I use this to apply damage to all valid targets.
In my use case, I take the target data and apply a random integer value as damage to the hit actors. My overall damage pipeline is still a work in progress, but I needed to put some together as a proof of concept.
Below are the C++ classes, and example images from Blueprint on how I implement the Wait Target Data task for my project:
NOTE: Since my project is a top-down/isometric point & click example, I needed to create my own GameplayAbilityTargetActor class so that I could properly trace from the camera to the ground (with its own unique TargetTraceChannel) and draw a cylindrical overlap from the hit location.
Features
Custom Debug Drawing
In order to help me debug the traces, I added two different traces, a line trace to show me the impact normal of the hit trace from the camera to the ground, and a cylinder overlap draw to show me the radius and bounds of my overlap detection. Here is the code found in the method FHitResult AGASCourseTargetActor_CameraTrace::PerformTrace(AActor* InSourceActor)
#if ENABLE_DRAW_DEBUG
if (bDebug)
{
const FVector CylinderHeight = (ReturnHitResult.Normal * CollisionHeight);
DrawDebugCylinder(ThisWorld, TraceEnd, TraceEnd + CylinderHeight, CollisionRadius, 10, FColor::Red, false, 1.0f, 0, 2.0f);
DrawDebugLine(GetWorld(), ReturnHitResult.Location, ReturnHitResult.Location + (ReturnHitResult.Normal * 500.0f), FColor::Blue, true);
}
#endif
Custom Character Outline
To help players know that an actor within the radius of the marker are indeed valid and will receive damage (or any other effect) on confirmation, I have added a custom character outline that I enable/disable on tick, check it out in the method: void AGASCourseTargetActor_CameraTrace::Tick(float DeltaSeconds)
Here are some references for those interested in the topic of creating this outline via post process:
Tom Looman - Multi-color Outline Post Process in Unreal Engine
BYC - [Outline Effect]
Matt Aspland - How To Highlight An Object With An Outline In Unreal Engine 5 (Tutorial)
I use a customm struct to initialize my outline data:
For the future, I’d like to move this out to some project settings so that you can parameterize the outline color either based on class, or even team association (Hostile, Neutral, Friendly).
void AGASCourseTargetActor_CameraTrace::DrawTargetOutline(TArray<TWeakObjectPtr<AActor>> InHitActors,
TArray<TWeakObjectPtr<AActor>> InLatestHitActors)
{
if(TargetOutlineData.CharacterClassToOutline == nullptr)
{
return;
}
for(const TWeakObjectPtr<AActor>& Actor : InHitActors)
{
if(Actor->IsA(TargetOutlineData.CharacterClassToOutline))
{
const AGASCourseCharacter* Character = Cast<AGASCourseCharacter>(Actor);
if(USkeletalMeshComponent* Mesh = Character->GetComponentByClass<USkeletalMeshComponent>())
{
Mesh->SetRenderCustomDepth(false);
Mesh->SetCustomDepthStencilValue(0);
}
}
}
for(const TWeakObjectPtr<AActor>& Actor : InLatestHitActors)
{
if(Actor->IsA(TargetOutlineData.CharacterClassToOutline))
{
const AGASCourseCharacter* Character = Cast<AGASCourseCharacter>(Actor);
if(USkeletalMeshComponent* Mesh = Character->GetComponentByClass<USkeletalMeshComponent>())
{
Mesh->SetRenderCustomDepth(true);
Mesh->SetCustomDepthStencilValue(2);
}
}
}
}
void AGASCourseTargetActor_CameraTrace::ClearTargetOutline(TArray<TWeakObjectPtr<AActor>> InHitActors)
{
if(TargetOutlineData.CharacterClassToOutline == nullptr)
{
return;
}
for(const TWeakObjectPtr<AActor>& Actor : InHitActors)
{
if(Actor->IsA(TargetOutlineData.CharacterClassToOutline))
{
const AGASCourseCharacter* Character = Cast<AGASCourseCharacter>(Actor);
if(USkeletalMeshComponent* Mesh = Character->GetComponentByClass<USkeletalMeshComponent>())
{
Mesh->SetRenderCustomDepth(false);
Mesh->SetCustomDepthStencilValue(0);
}
}
}
}
Custom Reticle
The last thing that I needed was a custom reticle so that players can visualize the radius of the aim cast spell. This reticle is a custom decal material that gets drawn via the AGameplayAbilityWorldReticle class. I use the WorldReticleParameters to adjust the scale of the actor:
You can find where I initialize the world reticle in the method FHitResult AGASCourseTargetActor_CameraTrace::PerformTrace(AActor* InSourceActor)
if (AGameplayAbilityWorldReticle* LocalReticleActor = ReticleActor.Get())
{
LocalReticleActor->SetIsTargetValid(bLastTraceWasGood);
LocalReticleActor->SetActorLocation(ReturnHitResult.Location);
LocalReticleActor->SetActorScale3D(ReticleParams.AOEScale);
FRotator LocalReticleRot = ReturnHitResult.Normal.Rotation();
LocalReticleActor->SetActorRotation(LocalReticleRot);
}
return ReturnHitResult;
Here is the reference I used to create the material:
Segmented Circle Material in Unreal Engine 4
To wrap up this blog post, here are the final results with all the C++ code I wrote and images of the BP implementation of my Aim Cast ability.
Example videos:
Blueprints
C++:
GASCourseTargetActor_Trace.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "GASCourseCharacter.h"
#include "Abilities/GameplayAbilityTargetActor_Trace.h"
#include "GASCourseTargetActor_Trace.generated.h"
/**
*
*/
USTRUCT(Blueprintable)
struct FTargetingOutline
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere,BlueprintReadWrite, Category = "GASCourse|Targeting|Outline")
bool bEnableTargetingOutline;
UPROPERTY(EditAnywhere,BlueprintReadWrite, Category = "GASCourse|Targeting|Outline")
FLinearColor OutlineColor;
UPROPERTY(EditAnywhere,BlueprintReadWrite, Category = "GASCourse|Targeting|Outline")
TSubclassOf<AGASCourseCharacter> CharacterClassToOutline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GASCourse|Targeting|Outline")
UMaterialInterface* OutlineMaterial;
FTargetingOutline()
{
bEnableTargetingOutline = true;
OutlineColor = FColor::Red;
CharacterClassToOutline = nullptr;
OutlineMaterial = nullptr;
}
};
UCLASS(Abstract, Blueprintable, notplaceable, config=Game, HideCategories=Trace)
class GASCOURSE_API AGASCourseTargetActor_Trace : public AGameplayAbilityTargetActor
{
GENERATED_UCLASS_BODY()
public:
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void CancelTargeting() override;
virtual void ConfirmTargeting() override;
/** Traces as normal, but will manually filter all hit actors */
static void LineTraceWithFilter(FHitResult& OutHitResult, const UWorld* World, const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, ECollisionChannel CollisionChannel, const FCollisionQueryParams Params);
/** Sweeps as normal, but will manually filter all hit actors */
static void SweepWithFilter(FHitResult& OutHitResult, const UWorld* World, const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, const FQuat& Rotation, const FCollisionShape CollisionShape, FName ProfileName, const FCollisionQueryParams Params);
void AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params, const FVector& TraceStart, FVector& OutTraceEnd, bool bIgnorePitch = false) const;
static bool ClipCameraRayToAbilityRange(FVector CameraLocation, FVector CameraDirection, FVector AbilityCenter, float AbilityRange, FVector& ClippedPosition);
virtual void StartTargeting(UGameplayAbility* Ability) override;
virtual void ConfirmTargetingAndContinue() override;
virtual void Tick(float DeltaSeconds) override;
virtual void ShowMouseCursor(bool bShowCursor);
//Range of the required trace to hit the ground/landscepe
float MaxRange;
//Profile FName to detect the ground
UPROPERTY(BlueprintReadOnly, meta = (ExposeOnSpawn = true), Category = Targeting)
TEnumAsByte<ECollisionChannel> TraceChannel;
TArray<TWeakObjectPtr<AActor>> ActorsToOutline;
protected:
virtual FHitResult PerformTrace(AActor* InSourceActor) PURE_VIRTUAL(AGameplayAbilityTargetActor_Trace, return FHitResult(););
FGameplayAbilityTargetDataHandle MakeTargetData(const FHitResult& HitResult) const;
TWeakObjectPtr<AGameplayAbilityWorldReticle> ReticleActor;
virtual void UpdateLooseGameplayTagsDuringTargeting(FGameplayTag InGameplayTag, int32 InCount);
virtual void DrawTargetOutline(TArray<TWeakObjectPtr<AActor> > InHitActors, TArray<TWeakObjectPtr<AActor>> InLatestHitActors);
virtual void ClearTargetOutline(TArray<TWeakObjectPtr<AActor> > InHitActors);
protected:
FGameplayTagContainer DefaultTargetingTagContainer;
};
GASCourseTargetActor_Trace.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "Game/GameplayAbilitySystem/Tasks/AbilityTargetActor/GASCourseTargetActor_Trace.h"
#include "AbilitySystemComponent.h"
#include "Game/Character/NPC/GASCourseNPC_Base.h"
#include "Game/Character/Player/GASCoursePlayerController.h"
#include "Abilities/GameplayAbility.h"
#include "Game/GameplayAbilitySystem/GASCourseNativeGameplayTags.h"
AGASCourseTargetActor_Trace::AGASCourseTargetActor_Trace(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickGroup = TG_PostUpdateWork;
//Initialize these variables to our needs for tracing.
MaxRange = 999999.0f;
}
void AGASCourseTargetActor_Trace::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (ReticleActor.IsValid())
{
ReticleActor.Get()->Destroy();
}
Super::EndPlay(EndPlayReason);
}
void AGASCourseTargetActor_Trace::LineTraceWithFilter(FHitResult& OutHitResult, const UWorld* World,
const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, ECollisionChannel CollisionChannel,
const FCollisionQueryParams Params)
{
check(World);
TArray<FHitResult> HitResults;
//World->LineTraceMultiByProfile(HitResults, Start, End, ProfileName, Params);
World->LineTraceMultiByChannel(HitResults, Start, End, CollisionChannel, Params);
OutHitResult.TraceStart = Start;
OutHitResult.TraceEnd = End;
for (int32 HitIdx = 0; HitIdx < HitResults.Num(); ++HitIdx)
{
const FHitResult& Hit = HitResults[HitIdx];
if (!Hit.HitObjectHandle.IsValid() || FilterHandle.FilterPassesForActor(Hit.HitObjectHandle.FetchActor()))
{
OutHitResult = Hit;
OutHitResult.bBlockingHit = true; // treat it as a blocking hit
return;
}
}
}
void AGASCourseTargetActor_Trace::SweepWithFilter(FHitResult& OutHitResult, const UWorld* World,
const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, const FQuat& Rotation,
const FCollisionShape CollisionShape, FName ProfileName, const FCollisionQueryParams Params)
{
check(World);
TArray<FHitResult> HitResults;
World->SweepMultiByProfile(HitResults, Start, End, Rotation, ProfileName, CollisionShape, Params);
OutHitResult.TraceStart = Start;
OutHitResult.TraceEnd = End;
for (int32 HitIdx = 0; HitIdx < HitResults.Num(); ++HitIdx)
{
const FHitResult& Hit = HitResults[HitIdx];
if (!Hit.HitObjectHandle.IsValid() || FilterHandle.FilterPassesForActor(Hit.HitObjectHandle.FetchActor()))
{
OutHitResult = Hit;
OutHitResult.bBlockingHit = true; // treat it as a blocking hit
return;
}
}
}
void AGASCourseTargetActor_Trace::AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params,
const FVector& TraceStart, FVector& OutTraceEnd, bool bIgnorePitch) const
{
if (!OwningAbility) // Server and launching client only
{
return;
}
APlayerController* PC = OwningAbility->GetCurrentActorInfo()->PlayerController.Get();
check(PC);
FVector ViewStart;
FRotator ViewRot;
PC->GetPlayerViewPoint(ViewStart, ViewRot);
const FVector ViewDir = ViewRot.Vector();
FVector ViewEnd = ViewStart + (ViewDir * MaxRange);
ClipCameraRayToAbilityRange(ViewStart, ViewDir, TraceStart, MaxRange, ViewEnd);
FHitResult HitResult;
LineTraceWithFilter(HitResult, InSourceActor->GetWorld(), Filter, ViewStart, ViewEnd, TraceChannel, Params);
const bool bUseTraceResult = HitResult.bBlockingHit && (FVector::DistSquared(TraceStart, HitResult.Location) <= (MaxRange * MaxRange));
const FVector AdjustedEnd = (bUseTraceResult) ? HitResult.Location : ViewEnd;
FVector AdjustedAimDir = (AdjustedEnd - TraceStart).GetSafeNormal();
if (AdjustedAimDir.IsZero())
{
AdjustedAimDir = ViewDir;
}
if (bUseTraceResult)
{
FVector OriginalAimDir = (ViewEnd - TraceStart).GetSafeNormal();
if (!OriginalAimDir.IsZero())
{
// Convert to angles and use original pitch
const FRotator OriginalAimRot = OriginalAimDir.Rotation();
FRotator AdjustedAimRot = AdjustedAimDir.Rotation();
AdjustedAimRot.Pitch = OriginalAimRot.Pitch;
AdjustedAimDir = AdjustedAimRot.Vector();
}
}
OutTraceEnd = TraceStart + (AdjustedAimDir * MaxRange);
}
bool AGASCourseTargetActor_Trace::ClipCameraRayToAbilityRange(FVector CameraLocation, FVector CameraDirection,
FVector AbilityCenter, float AbilityRange, FVector& ClippedPosition)
{
FVector CameraToCenter = AbilityCenter - CameraLocation;
float DotToCenter = FVector::DotProduct(CameraToCenter, CameraDirection);
if (DotToCenter >= 0) //If this fails, we're pointed away from the center, but we might be inside the sphere and able to find a good exit point.
{
float DistanceSquared = CameraToCenter.SizeSquared() - (DotToCenter * DotToCenter);
float RadiusSquared = (AbilityRange * AbilityRange);
if (DistanceSquared <= RadiusSquared)
{
float DistanceFromCamera = FMath::Sqrt(RadiusSquared - DistanceSquared);
float DistanceAlongRay = DotToCenter + DistanceFromCamera; //Subtracting instead of adding will get the other intersection point
ClippedPosition = CameraLocation + (DistanceAlongRay * CameraDirection); //Cam aim point clipped to range sphere
return true;
}
}
return false;
}
void AGASCourseTargetActor_Trace::Tick(float DeltaSeconds)
{
// very temp - do a mostly hardcoded trace from the source actor
if (SourceActor && SourceActor->GetLocalRole() != ENetRole::ROLE_SimulatedProxy)
{
FHitResult HitResult = PerformTrace(SourceActor);
FVector EndPoint = HitResult.Component.IsValid() ? HitResult.ImpactPoint : HitResult.TraceEnd;
#if ENABLE_DRAW_DEBUG
if (bDebug)
{
DrawDebugLine(GetWorld(), SourceActor->GetActorLocation(), EndPoint, FColor::Green, false);
DrawDebugSphere(GetWorld(), EndPoint, 16, 10, FColor::Green, false);
}
#endif // ENABLE_DRAW_DEBUG
SetActorLocationAndRotation(EndPoint, SourceActor->GetActorRotation());
}
}
void AGASCourseTargetActor_Trace::ShowMouseCursor(bool bShowCursor)
{
if(AGASCoursePlayerController* SourcePC = Cast<AGASCoursePlayerController>(OwningAbility->GetCurrentActorInfo()->PlayerController.Get()))
{
SourcePC->bShowMouseCursor = bShowCursor;
}
}
FGameplayAbilityTargetDataHandle AGASCourseTargetActor_Trace::MakeTargetData(const FHitResult& HitResult) const
{
/** Note: This will be cleaned up by the FGameplayAbilityTargetDataHandle (via an internal TSharedPtr) */
return StartLocation.MakeTargetDataHandleFromHitResult(OwningAbility, HitResult);
}
void AGASCourseTargetActor_Trace::DrawTargetOutline(TArray<TWeakObjectPtr<AActor>> InHitActors, TArray<TWeakObjectPtr<AActor>> InLatestHitActors)
{
}
void AGASCourseTargetActor_Trace::ClearTargetOutline(TArray<TWeakObjectPtr<AActor>> InHitActors)
{
}
void AGASCourseTargetActor_Trace::StartTargeting(UGameplayAbility* InAbility)
{
Super::StartTargeting(InAbility);
SourceActor = InAbility->GetCurrentActorInfo()->AvatarActor.Get();
UpdateLooseGameplayTagsDuringTargeting(Status_Block_PointClickMovementInput, 1);
UpdateLooseGameplayTagsDuringTargeting(Status_Gameplay_Targeting, 1);
ShowMouseCursor(false);
if (ReticleClass)
{
AGameplayAbilityWorldReticle* SpawnedReticleActor = GetWorld()->SpawnActor<AGameplayAbilityWorldReticle>(ReticleClass, GetActorLocation(), GetActorRotation());
if (SpawnedReticleActor)
{
SpawnedReticleActor->InitializeReticle(this, PrimaryPC, ReticleParams);
ReticleActor = SpawnedReticleActor;
// This is to catch cases of playing on a listen server where we are using a replicated reticle actor.
// (In a client controlled player, this would only run on the client and therefor never replicate. If it runs
// on a listen server, the reticle actor may replicate. We want consistancy between client/listen server players.
// Just saying 'make the reticle actor non replicated' isnt a good answer, since we want to mix and match reticle
// actors and there may be other targeting types that want to replicate the same reticle actor class).
if (!ShouldProduceTargetDataOnServer)
{
SpawnedReticleActor->SetReplicates(false);
}
}
}
}
void AGASCourseTargetActor_Trace::ConfirmTargetingAndContinue()
{
Super::ConfirmTargetingAndContinue();
UpdateLooseGameplayTagsDuringTargeting(Status_Block_PointClickMovementInput, 0);
UpdateLooseGameplayTagsDuringTargeting(Status_Gameplay_Targeting, 0);
ShowMouseCursor(true);
ClearTargetOutline(ActorsToOutline);
}
void AGASCourseTargetActor_Trace::CancelTargeting()
{
Super::CancelTargeting();
UpdateLooseGameplayTagsDuringTargeting(Status_Block_PointClickMovementInput, 0);
UpdateLooseGameplayTagsDuringTargeting(Status_Gameplay_Targeting, 0);
ShowMouseCursor(true);
ClearTargetOutline(ActorsToOutline);
}
void AGASCourseTargetActor_Trace::ConfirmTargeting()
{
Super::ConfirmTargeting();
Super::ConfirmTargetingAndContinue();
UpdateLooseGameplayTagsDuringTargeting(Status_Block_PointClickMovementInput, 0);
UpdateLooseGameplayTagsDuringTargeting(Status_Gameplay_Targeting, 0);
ShowMouseCursor(true);
ClearTargetOutline(ActorsToOutline);
}
void AGASCourseTargetActor_Trace::UpdateLooseGameplayTagsDuringTargeting(FGameplayTag InGameplayTag, int32 InCount)
{
if(UAbilitySystemComponent* ASC = OwningAbility->GetCurrentActorInfo()->AbilitySystemComponent.Get())
{
ASC->SetLooseGameplayTagCount(InGameplayTag, InCount);
}
}
GASCourseTargetActor_CameraTrace.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "GASCourseTargetActor_Trace.h"
#include "GASCourseTargetActor_CameraTrace.generated.h"
class UGameplayAbility;
/**
*
*/
UCLASS(Blueprintable)
class GASCOURSE_API AGASCourseTargetActor_CameraTrace : public AGASCourseTargetActor_Trace
{
GENERATED_UCLASS_BODY()
public:
virtual void StartTargeting(UGameplayAbility* InAbility) override;
virtual void ConfirmTargetingAndContinue() override;
virtual void CancelTargeting();
virtual void Tick(float DeltaSeconds) override;
/** Radius for a sphere or capsule. */
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Targeting)
float CollisionRadius;
/** Height for a capsule. Implicitly indicates a capsule is desired if this is greater than zero. */
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Targeting)
float CollisionHeight;
/** Trace Channel to check for*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Targeting)
TArray<TEnumAsByte<ECollisionChannel>> QueryChannels;
/** Trace Channel to check for*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Targeting)
FTargetingOutline TargetOutlineData;
protected:
virtual FHitResult PerformTrace(AActor* InSourceActor) override;
virtual bool IsConfirmTargetingAllowed() override;
TArray<TWeakObjectPtr<AActor> > PerformOverlap(const FVector& Origin);
bool OverlapMultiByObjectTypes(TArray<TWeakObjectPtr<AActor>>& OutHitActors, const FVector& Pos, const FQuat& Rot, const FCollisionShape& OverlapCollisionShape,
const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam) const;
FGameplayAbilityTargetDataHandle MakeTargetData(const TArray<TWeakObjectPtr<AActor>>& Actors, const FVector& Origin) const;
void DrawTargetOutline(TArray<TWeakObjectPtr<AActor>> InHitActors, TArray<TWeakObjectPtr<AActor>> InLatestHitActors) override;
void ClearTargetOutline(TArray<TWeakObjectPtr<AActor>> InHitActors) override;
protected:
bool bLastTraceWasGood;
};
GASCourseTargetActor_CameraTrace.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "Game/GameplayAbilitySystem/Tasks/AbilityTargetActor/GASCourseTargetActor_CameraTrace.h"
#include "Abilities/GameplayAbility.h"
AGASCourseTargetActor_CameraTrace::AGASCourseTargetActor_CameraTrace(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
CollisionRadius = 50.0f;
CollisionHeight = 50.0f;
}
void AGASCourseTargetActor_CameraTrace::StartTargeting(UGameplayAbility* InAbility)
{
Super::StartTargeting(InAbility);
}
void AGASCourseTargetActor_CameraTrace::ConfirmTargetingAndContinue()
{
check(ShouldProduceTargetData());
if (SourceActor)
{
const FVector Origin = PerformTrace(SourceActor).Location;
FGameplayAbilityTargetDataHandle Handle = MakeTargetData(PerformOverlap(Origin), Origin);
TargetDataReadyDelegate.Broadcast(Handle);
}
}
void AGASCourseTargetActor_CameraTrace::CancelTargeting()
{
Super::CancelTargeting();
}
void AGASCourseTargetActor_CameraTrace::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (SourceActor && SourceActor->GetLocalRole() != ENetRole::ROLE_SimulatedProxy)
{
const FHitResult HitResult = PerformTrace(SourceActor);
const FVector EndPoint = HitResult.Component.IsValid() ? HitResult.ImpactPoint : HitResult.TraceEnd;
if(TargetOutlineData.bEnableTargetingOutline)
{
DrawTargetOutline(ActorsToOutline, PerformOverlap(EndPoint));
ActorsToOutline = PerformOverlap(EndPoint);
}
}
}
FHitResult AGASCourseTargetActor_CameraTrace::PerformTrace(AActor* InSourceActor)
{
bool bTraceComplex = false;
FCollisionQueryParams Params(SCENE_QUERY_STAT(AGASCourseTargetActor_CameraTrace), bTraceComplex);
Params.bReturnPhysicalMaterial = true;
Params.AddIgnoredActor(InSourceActor);
UWorld *ThisWorld = GetWorld();
FHitResult ReturnHitResult;
APlayerController* PC = OwningAbility->GetCurrentActorInfo()->PlayerController.Get();
check(PC);
FVector MousePositionToWorldLocation;
FVector MousePositionToWorldDirection;
FVector TraceStart;
FVector TraceEnd;
bLastTraceWasGood = false;
if(PC->DeprojectMousePositionToWorld(MousePositionToWorldLocation, MousePositionToWorldDirection))
{
TraceStart = MousePositionToWorldLocation;
TraceEnd = TraceStart + MousePositionToWorldDirection * MaxRange;
}
LineTraceWithFilter(ReturnHitResult, InSourceActor->GetWorld(), Filter, TraceStart, TraceEnd, TraceChannel, Params);
//Default to end of trace line if we don't hit anything.
if (ReturnHitResult.bBlockingHit)
{
TraceEnd = ReturnHitResult.Location;
bLastTraceWasGood = true;
}
#if ENABLE_DRAW_DEBUG
if (bDebug)
{
const FVector CylinderHeight = (ReturnHitResult.Normal * CollisionHeight);
DrawDebugCylinder(ThisWorld, TraceEnd, TraceEnd + CylinderHeight, CollisionRadius, 10, FColor::Red, false, 1.0f, 0, 2.0f);
DrawDebugLine(GetWorld(), ReturnHitResult.Location, ReturnHitResult.Location + (ReturnHitResult.Normal * 500.0f), FColor::Blue, true);
}
#endif
if (AGameplayAbilityWorldReticle* LocalReticleActor = ReticleActor.Get())
{
LocalReticleActor->SetIsTargetValid(bLastTraceWasGood);
LocalReticleActor->SetActorLocation(ReturnHitResult.Location);
LocalReticleActor->SetActorScale3D(ReticleParams.AOEScale);
FRotator LocalReticleRot = ReturnHitResult.Normal.Rotation();
LocalReticleActor->SetActorRotation(LocalReticleRot);
}
return ReturnHitResult;
}
bool AGASCourseTargetActor_CameraTrace::IsConfirmTargetingAllowed()
{
return bLastTraceWasGood;
}
TArray<TWeakObjectPtr<AActor>> AGASCourseTargetActor_CameraTrace::PerformOverlap(const FVector& Origin)
{
constexpr bool bTraceComplex = false;
FCollisionQueryParams Params(SCENE_QUERY_STAT(RadiusTargetingOverlap), bTraceComplex);
Params.bReturnPhysicalMaterial = false;
TArray<TWeakObjectPtr<AActor>> HitActors;
if(QueryChannels.Num() == 0)
{
return HitActors;
}
OverlapMultiByObjectTypes(HitActors, Origin, FQuat::Identity, FCollisionShape::MakeSphere(CollisionRadius), Params);
return HitActors;
}
bool AGASCourseTargetActor_CameraTrace::OverlapMultiByObjectTypes(TArray<TWeakObjectPtr<AActor>>& OutHitActors, const FVector& Pos,
const FQuat& Rot, const FCollisionShape& OverlapCollisionShape, const FCollisionQueryParams& Params) const
{
TArray<FOverlapResult> Overlaps;
bool bTraceSuccessful = false;
if(QueryChannels.Num() == 0)
{
return bTraceSuccessful;
}
for(const ECollisionChannel QueryChannel : QueryChannels)
{
SourceActor->GetWorld()->OverlapMultiByObjectType(Overlaps, Pos, Rot, FCollisionObjectQueryParams(QueryChannel), OverlapCollisionShape, Params);
for(int32 i = 0; i < Overlaps.Num(); ++i)
{
//Should this check to see if these pawns are in the AimTarget list?
AActor* HitActor = Overlaps[i].OverlapObjectHandle.FetchActor<AActor>();
if (HitActor && !OutHitActors.Contains(HitActor) && Filter.FilterPassesForActor(HitActor))
{
OutHitActors.Add(HitActor);
}
}
}
return bTraceSuccessful = OutHitActors.Num() > 0 ? true : false;
}
FGameplayAbilityTargetDataHandle AGASCourseTargetActor_CameraTrace::MakeTargetData(
const TArray<TWeakObjectPtr<AActor>>& Actors, const FVector& Origin) const
{
if (OwningAbility)
{
/** Use the source location instead of the literal origin */
return StartLocation.MakeTargetDataHandleFromActors(Actors, false);
}
return FGameplayAbilityTargetDataHandle();
}
void AGASCourseTargetActor_CameraTrace::DrawTargetOutline(TArray<TWeakObjectPtr<AActor>> InHitActors,
TArray<TWeakObjectPtr<AActor>> InLatestHitActors)
{
if(TargetOutlineData.CharacterClassToOutline == nullptr)
{
return;
}
for(const TWeakObjectPtr<AActor>& Actor : InHitActors)
{
if(Actor->IsA(TargetOutlineData.CharacterClassToOutline))
{
const AGASCourseCharacter* Character = Cast<AGASCourseCharacter>(Actor);
if(USkeletalMeshComponent* Mesh = Character->GetComponentByClass<USkeletalMeshComponent>())
{
Mesh->SetRenderCustomDepth(false);
Mesh->SetCustomDepthStencilValue(0);
}
}
}
for(const TWeakObjectPtr<AActor>& Actor : InLatestHitActors)
{
if(Actor->IsA(TargetOutlineData.CharacterClassToOutline))
{
const AGASCourseCharacter* Character = Cast<AGASCourseCharacter>(Actor);
if(USkeletalMeshComponent* Mesh = Character->GetComponentByClass<USkeletalMeshComponent>())
{
Mesh->SetRenderCustomDepth(true);
Mesh->SetCustomDepthStencilValue(2);
}
}
}
}
void AGASCourseTargetActor_CameraTrace::ClearTargetOutline(TArray<TWeakObjectPtr<AActor>> InHitActors)
{
if(TargetOutlineData.CharacterClassToOutline == nullptr)
{
return;
}
for(const TWeakObjectPtr<AActor>& Actor : InHitActors)
{
if(Actor->IsA(TargetOutlineData.CharacterClassToOutline))
{
const AGASCourseCharacter* Character = Cast<AGASCourseCharacter>(Actor);
if(USkeletalMeshComponent* Mesh = Character->GetComponentByClass<USkeletalMeshComponent>())
{
Mesh->SetRenderCustomDepth(false);
Mesh->SetCustomDepthStencilValue(0);
}
}
}
}
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:
Gameplay Ability System & Enhanced Input Setup