Gameplay Ability System Course Project - Development Blog

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 : https://youtube.com/@jevinscherries?si=GKt32rDN6z2wn5xV
Twitter : https://twitter.com/jevincherries
Instagram : Jevins Cherries Gamedev (@jevinscherriesgamedev) • Instagram photos and videos

Today we are going to talk about how I handle the mechanic of Status Effects in the GAS Course Project, using Gameplay Effects! Additionally, I will showcase how I use a custom GameplayEffectUIData class to draw unique icons and descriptions for the status effects on screen. As always, I highly recommend reading through Tranek’s documentation found here: GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project.


EffectDescription

Classes to research on your own:

UGameplayEffect: https://docs.unrealengine.com/4.26/en-US/BlueprintAPI/GameplayEffects/
UGameplayEffectUIData: https://docs.unrealengine.com/4.27/en-US/API/Plugins/GameplayAbilities/UGameplayEffectUIData/

Additional Reading:


What are Status Effects?

In the most simplest terms, Status Effects are any type of passive effect that can be applied to a character that can alter behaviors or multiple attributes associated with that character either for a short duration or permanently. I recommend watching this video by the channel Design Doc for a more in-depth discussion about Status Effects: https://www.youtube.com/watch?v=ThDVGP4UB30.

In the context of the GAS Course Project, I wanted to create a base system that allows for all types of status effects to be applied to characters, starting with NPCs, that can allow for appropriate scalability and UI reflection for the type of ARPG I am intending to demonstrate. Luckily, the Gameplay Ability System was built with the idea of status effects in mind with the use of Gameplay Effects and so I primarily use Gameplay Effects, in addition to unique classes to handle status effects and their dynamic descriptions, to create a base that I believe can work for my intended use-cases. Let’s start with the GASCStatusEffectListener component class.


From Tranek:

“Cannot predict the removal of GameplayEffects. We can however predict adding GameplayEffects with the inverse effects, effectively removing them. This is not always appropriate or feasible and still remains an issue.”

The primary concern when developing the system for status effects was making sure both the application and the removal of the status effects were properly replicated to server and clients. To best handle this, I created a component class to listen for the application and removal of Gameplay Effects with the parent asset tag, Effect.AssetTag.Status, and broadcast these events to everyone. Here is the code for that class:

GASCStatusEffectListener.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ActiveGameplayEffectHandle.h"
#include "GameplayEffectTypes.h"
#include "GameplayEffect.h"
#include "Components/ActorComponent.h"
#include "GASCStatusEffectListenerComp.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FStatusEffectApplied, FActiveGameplayEffectHandle, StatusEffectSpec);

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent), Blueprintable )
class GASCOURSE_API UGASCStatusEffectListenerComp : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UGASCStatusEffectListenerComp();

	UFUNCTION(NetMulticast, Reliable)
	void OnStatusEffectApplied(UAbilitySystemComponent* Source, const FGameplayEffectSpec& GameplayEffectSpec, FActiveGameplayEffectHandle ActiveGameplayEffectHandle);

	UFUNCTION(Server, Reliable)
	void OnStatusEffectRemoved(const FActiveGameplayEffect& ActiveGameplayEffect);
	
	UPROPERTY(BlueprintAssignable)
	FStatusEffectApplied OnStatusEffectAppliedHandle;

	UPROPERTY(BlueprintAssignable)
	FStatusEffectApplied OnStatusEffectRemovedHandle;

	UPROPERTY(EditAnywhere, Category = "GASCourse|StatusEffect|Tags")
	FGameplayTag StatusEffectAssetTag;

	UFUNCTION()
	void ApplyDefaultActiveStatusEffects();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

	virtual void Deactivate() override;
};

GASCStatusEffectListener.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "Game/Character/Components/GASCStatusEffectListenerComp.h"
#include "GASCourseCharacter.h"

// Sets default values for this component's properties
UGASCStatusEffectListenerComp::UGASCStatusEffectListenerComp()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = false;

	// ...
}
void UGASCStatusEffectListenerComp::OnStatusEffectRemoved_Implementation(const FActiveGameplayEffect& ActiveGameplayEffect)
{
	FGameplayTagContainer GameplayEffectAssetTags;
	ActiveGameplayEffect.Spec.GetAllAssetTags(GameplayEffectAssetTags);

	if(GameplayEffectAssetTags.IsEmpty())
	{
		return;
	}

	if(GameplayEffectAssetTags.HasTag(StatusEffectAssetTag))
	{
		OnStatusEffectRemovedHandle.Broadcast(ActiveGameplayEffect.Handle);
	}
}

void UGASCStatusEffectListenerComp::OnStatusEffectApplied_Implementation(UAbilitySystemComponent* Source, const FGameplayEffectSpec& GameplayEffectSpec, FActiveGameplayEffectHandle ActiveGameplayEffectHandle)
{
	FGameplayTagContainer GameplayEffectAssetTags;
	GameplayEffectSpec.GetAllAssetTags(GameplayEffectAssetTags);

	if(GameplayEffectAssetTags.IsEmpty())
	{
		return;
	}

	if(GameplayEffectAssetTags.HasTag(StatusEffectAssetTag))
	{
		OnStatusEffectAppliedHandle.Broadcast(ActiveGameplayEffectHandle);
	}
}

void UGASCStatusEffectListenerComp::ApplyDefaultActiveStatusEffects()
{
	if(const AGASCourseCharacter* OwningCharacter = Cast<AGASCourseCharacter>(GetOwner()))
	{
		if(const UAbilitySystemComponent* ASC = OwningCharacter->GetAbilitySystemComponent())
		{	
			TArray<FActiveGameplayEffectHandle> ActiveHandles = ASC->GetActiveEffectsWithAllTags(StatusEffectAssetTag.GetSingleTagContainer());
			for(const FActiveGameplayEffectHandle InActiveHandle : ActiveHandles)
			{
				OnStatusEffectAppliedHandle.Broadcast(InActiveHandle);
			}
		}
	}
}

// Called when the game starts
void UGASCStatusEffectListenerComp::BeginPlay()
{
	Super::BeginPlay();

	if(const AGASCourseCharacter* OwningCharacter = Cast<AGASCourseCharacter>(GetOwner()))
	{
		if(UAbilitySystemComponent* ASC = OwningCharacter->GetAbilitySystemComponent())
		{
			ASC->OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &ThisClass::OnStatusEffectApplied);
			ASC->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &ThisClass::OnStatusEffectRemoved);
		}
	}
}

void UGASCStatusEffectListenerComp::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	if(OnStatusEffectAppliedHandle.IsBound())
	{
		OnStatusEffectAppliedHandle.Clear();
	}
	if(OnStatusEffectRemovedHandle.IsBound())
	{
		OnStatusEffectRemovedHandle.Clear();
	}
	
	Super::EndPlay(EndPlayReason);
}


void UGASCStatusEffectListenerComp::Deactivate()
{
	if(OnStatusEffectAppliedHandle.IsBound())
	{
		OnStatusEffectAppliedHandle.Clear();
	}
	if(OnStatusEffectRemovedHandle.IsBound())
	{
		OnStatusEffectRemovedHandle.Clear();
	}
	
	Super::Deactivate();
}

The key here is that we are listening for and broadcasting the application/removal events associated with Gameplay Effects that have Asset Tags that follow the tag hierarchal paradigm, Effect.AssetTag.Status, as shown in the component setup in Blueprint:

StatusEffectListener_TagSetup

In cases where a Status Effect might be applied immediately on Begin Play by a character, I created a function to handle this called ApplyDefaultActiveStatusEffects which is then called on BeginPlay() of my GASCourseCharacter class.

Then, in my base GASCourseCharacter class, I add the following code so that I can initialize its component and receive the necessary callbacks for status effect application and removal:

GASCourseCharacter.h

	/** The component responsible for listening to status effect events.
	 *
	 * This component is used to listen to status effect events, such as when a status effect is applied or removed from the character.
	 * It is visible anywhere and has read-only access, and falls under the StatusEffects category.
	 * The meta flag AllowPrivateAccess is set to true, allowing private access to this component.
	 */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = StatusEffects, meta = (AllowPrivateAccess = "true"))
	class UGASCStatusEffectListenerComp* StatusEffectListenerComp;

	public:

	UFUNCTION()
	void OnStatusEffectApplied(FActiveGameplayEffectHandle InStatusEffectApplied);

	UFUNCTION(BlueprintImplementableEvent)
	void OnStatusEffectApplied_Event(FActiveGameplayEffectHandle InStatusEffectApplied);

	UFUNCTION()
	void OnStatusEffectRemoved(FActiveGameplayEffectHandle InStatusEffectRemoved);
	
	UFUNCTION(NetMulticast, Reliable)
	void OnStatusEffectRemoved_Multicast(FActiveGameplayEffectHandle InStatusEffectRemoved);

	UFUNCTION(BlueprintImplementableEvent)
	void OnStatusEffectRemoved_Event(FActiveGameplayEffectHandle InStatusEffectRemoved);

GASCourseCharacter.cpp

AGASCourseCharacter::AGASCourseCharacter(const class FObjectInitializer& ObjectInitializer) :
	Super(ObjectInitializer.SetDefaultSubobjectClass<UGASCourseMovementComponent>(ACharacter::CharacterMovementComponentName))
{
	StatusEffectListenerComp = ObjectInitializer.CreateDefaultSubobject<UGASCStatusEffectListenerComp>(this, TEXT("StatusEffectListenerComp"));
	StatusEffectListenerComp->SetIsReplicated(true);
}


void AGASCourseCharacter::BeginPlay()
{
	// Call the base class  
	Super::BeginPlay();
	
	GameplayEffectAssetTagsToRemove.AddTag(FGameplayTag::RequestGameplayTag(FName("Effect.AssetTag.Status")));
	StatusEffectListenerComp->ApplyDefaultActiveStatusEffects();
}

void AGASCourseCharacter::OnStatusEffectApplied(FActiveGameplayEffectHandle InStatusEffectApplied)
{
	OnStatusEffectApplied_Event(InStatusEffectApplied);
}

void AGASCourseCharacter::OnStatusEffectRemoved(FActiveGameplayEffectHandle InStatusEffectRemoved)
{
	OnStatusEffectRemoved_Event(InStatusEffectRemoved);
	if(HasAuthority())
	{
		OnStatusEffectRemoved_Multicast(InStatusEffectRemoved);
	}
}

void AGASCourseCharacter::OnStatusEffectRemoved_Multicast_Implementation(FActiveGameplayEffectHandle InStatusEffectRemoved)
{
	OnStatusEffectRemoved_Event(InStatusEffectRemoved);
}

Finally, here is the Blueprint implementation inside of my BP_NPC_Base class:

We use these events to pass along the Gameplay Effect Handle to the NPC’s healthbar UI so that we can properly construct the effect description and assign the status effect icon to the UI widget Blueprint. We will discuss this more in detail later in this post.


GASCourseStatusEffectTable

In order to help me map certain damage types with specific Gameplay Effects, I created a new Data Asset class called GASCourseStatusEffectTable that does just that. This is a special setup that might require refactoring, or could be unnecessary if we say that designers are responsible for applying the appropriate effect after certain damage application; however, I wanted to try to automate in some way this process so that designers only have to worry about setting up the mapping data asset and then just applying the appropriate damage type.

Here is the code:

GASCourseStatusEffectTable.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "GameplayEffectTypes.h"
#include "GameplayEffect.h"
#include "GASCourseStatusEffectTable.generated.h"

class UAbilitySystemComponent;

/**
 * 
 */

USTRUCT()
struct FGameplayTagEventResponsePair
{
	GENERATED_USTRUCT_BODY()

	UPROPERTY(EditAnywhere, Category = "Response")
	FGameplayTag StatusEffectStateTag;
	
	UPROPERTY(EditAnywhere, Category = "Response")
	TSubclassOf<UGameplayEffect> ResponseGameplayEffect;
	
};

USTRUCT()
struct FGameplayTagEventResponseTableEntry
{
	GENERATED_USTRUCT_BODY()

	UPROPERTY(EditAnywhere, Category = "Response")
	FGameplayTag StatusEffectTag;
	
	/** Tags that count as "positive" toward to final response count. If the overall count is positive, this ResponseGameplayEffect is applied. */
	UPROPERTY(EditAnywhere, Category="Response")
	TArray<FGameplayTagEventResponsePair> StatusEffectTypes;
	
};

UCLASS()
class GASCOURSE_API UGASCourseStatusEffectTable : public UDataAsset
{
	GENERATED_UCLASS_BODY()

	UPROPERTY(EditAnywhere, Category="Response")
	TArray<FGameplayTagEventResponseTableEntry>	Entries;

	virtual void PostLoad() override;

	/**
	 * Function to apply a gameplay status effect to the target ability system component based on the provided status effect tags.
	 *
	 * @param TargetASC The ability system component to apply the status effect to.
	 * @param InstigatorASC The ability system component that is initiating the application of the status effect.
	 * @param StatusEffectTags The gameplay tags representing the status effect to be applied.
	 */
	UFUNCTION()
	void ApplyGameplayStatusEffect(UAbilitySystemComponent* TargetASC, UAbilitySystemComponent* InstigatorASC, const FGameplayTagContainer& StatusEffectTags);

protected:
	
	/**
	 * Check if any of the given status effect tags matches the entries in the status effect table.
	 *
	 * @param StatusEffectTags The tags to check against the status effect table entries.
	 * @param FoundStatusEffectEntry The found status effect entry when a match is found.
	 * @param FoundTag The found tag when a match is found.
	 * @return True if a match is found, false otherwise.
	 */
	bool HasMatchingStatusEffectTag(const FGameplayTagContainer& StatusEffectTags, FGameplayTagEventResponseTableEntry& FoundStatusEffectEntry, FGameplayTag& FoundTag);
	
};

GASCourseStatusEffectTable.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Game/GameplayAbilitySystem/GameplayTagResponseTable/GASCourseStatusEffectTable.h"
#include "AbilitySystemComponent.h"

UGASCourseStatusEffectTable::UGASCourseStatusEffectTable(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	
}

void UGASCourseStatusEffectTable::PostLoad()
{
	Super::PostLoad();
}

void UGASCourseStatusEffectTable::ApplyGameplayStatusEffect(UAbilitySystemComponent* TargetASC,
	UAbilitySystemComponent* InstigatorASC, const FGameplayTagContainer& StatusEffectTags)
{
	UE_LOG(LogTemp, Warning, TEXT("Tags: %s"), *StatusEffectTags.ToString());

	FGameplayTagEventResponseTableEntry FoundStatusEffectEntry;
	FGameplayTag FoundStatusTag;
	if(HasMatchingStatusEffectTag(StatusEffectTags, FoundStatusEffectEntry, FoundStatusTag))
	{
		for(FGameplayTagEventResponsePair StatusEffectPair : FoundStatusEffectEntry.StatusEffectTypes)
		{
			if(StatusEffectPair.StatusEffectStateTag == FoundStatusTag)
			{
				FActiveGameplayEffectHandle StatusEffectHandle = InstigatorASC->ApplyGameplayEffectToTarget(StatusEffectPair.ResponseGameplayEffect.GetDefaultObject(), TargetASC);
			}
		}
	}
}

bool UGASCourseStatusEffectTable::HasMatchingStatusEffectTag(const FGameplayTagContainer& StatusEffectTags,
	FGameplayTagEventResponseTableEntry& FoundStatusEffectEntry, FGameplayTag& FoundTag)
{
	bool bHasFoundTag = false;

	for(FGameplayTagEventResponseTableEntry Entry :Entries)
	{
		if(StatusEffectTags.HasTag(Entry.StatusEffectTag))
		{
			FoundStatusEffectEntry = Entry;
			for(FGameplayTag StatusTag : StatusEffectTags.GetGameplayTagArray())
			{
				ensure(StatusTag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("Effect.Gameplay.Status"))));
				if(StatusTag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("Effect.Gameplay.Status"))))
				{
					FoundTag = StatusTag;
					bHasFoundTag = true;
					break;
				}
			}
		}
	}

	return bHasFoundTag;
}

I initialize the GameplayStatusEffectTable in the character here:

GASCourseCharacter.cpp

void AGASCourseCharacter::InitializeAbilitySystem(UGASCourseAbilitySystemComponent* InASC)
{
	if(GetLocalRole() != ROLE_Authority || !InASC)
	{
		return;
	}
	if(DefaultAbilitySet)
	{
		DefaultAbilitySet->GiveToAbilitySystem(InASC, nullptr);
	}

	if(AbilityTagRelationshipMapping)
	{
		InASC->SetTagRelationshipMapping(AbilityTagRelationshipMapping);
	}

	if(GameplayStatusEffectTable)
	{
		InASC->SetGameplayEffectStatusTable(GameplayStatusEffectTable);
	}

	if(InASC)
	{
		InASC->AddGameplayEventTagContainerDelegate(FGameplayTagContainer(Event_OnDeath), FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this, &ThisClass::CharacterDeathGameplayEventCallback));
		InASC->RegisterGameplayTagEvent(FGameplayTag(Collision_IgnorePawn), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGASCourseCharacter::IgnorePawnCollisionGameplayTagEventCallback);
	}
}

GASCourseAbilitySystemComponent.h

	void SetGameplayEffectStatusTable(UGASCourseStatusEffectTable* NewStatusEffectTable);

	void ApplyGameplayStatusEffect(UAbilitySystemComponent* TargetASC, UAbilitySystemComponent* InstigatorASC, const FGameplayTagContainer& StatusEffectTags) const;

GASCourseAbilitySystemComponent.cpp

void UGASCourseAbilitySystemComponent::SetGameplayEffectStatusTable(UGASCourseStatusEffectTable* NewStatusEffectTable)
{
	if(NewStatusEffectTable)
	{
		GameplayStatusEffectTable = NewStatusEffectTable;
	}
}

void UGASCourseAbilitySystemComponent::ApplyGameplayStatusEffect(UAbilitySystemComponent* TargetASC,
	UAbilitySystemComponent* InstigatorASC, const FGameplayTagContainer& StatusEffectTags) const
{
	if(const AGASCourseCharacter* OwningCharacter = Cast<AGASCourseCharacter>(GetOwnerActor()))
	{
		if(OwningCharacter->GetGameplayStatusEffectTable())
		{
			GameplayStatusEffectTable->ApplyGameplayStatusEffect(TargetASC, InstigatorASC, StatusEffectTags);
		}
	}
}

Here is the damage application setup for when my projectile hits a target:

bool UGASCourseASCBlueprintLibrary::ApplyFireDamageToTarget(AActor* Target, AActor* Instigator, float Damage,
                                                            const FHitResult& HitResult, FDamageContext& DamageContext, bool bApplyBurnStack)
{
	DamageContext.DamageType = DamageType_Elemental_Fire;
	if(bApplyBurnStack)
	{
		FGameplayTagContainer GrantedTags;
		//TODO: Add this to Native Gameplay Tags
		GrantedTags.AddTag(FGameplayTag::RequestGameplayTag(FName("Effect.Gameplay.Status.Burn.Stack")));
		DamageContext.GrantedTags = GrantedTags;
	}
	DamageContext.HitResult = HitResult;
	
	constexpr FDamageOverTimeContext DamageOverTimeContext;
	UGameplayEffect* DamageEffect = ConstructDamageGameplayEffect(EGameplayEffectDurationType::Instant, DamageOverTimeContext);
	
	return ApplyDamageToTarget_Internal(Target, Instigator, Damage, DamageContext, DamageEffect);
}

bool UGASCourseASCBlueprintLibrary::ApplyDamageToTarget_Internal(AActor* Target, AActor* Instigator, float Damage,
                                                                 const FDamageContext& DamageContext, UGameplayEffect* GameplayEffect)
{
	if(!Instigator && !Target)
	{
		return false;
	}

	//TODO: Add check to verify ability system component + consider damage/health interface for Non-GAS actors
	if(UGASCourseAbilitySystemComponent* TargetASC = Target->GetComponentByClass<UGASCourseAbilitySystemComponent>())
	{
		if(UGASCourseAbilitySystemComponent* InstigatorASC = Instigator->GetComponentByClass<UGASCourseAbilitySystemComponent>())
		{
			if(UGASCourseGameplayEffect* DamageEffect = Cast<UGASCourseGameplayEffect>(GameplayEffect))
			{
							
				const int32 ExecutionIdx = DamageEffect->Executions.Num();
				DamageEffect->Executions.SetNum(ExecutionIdx + 1);
				FGameplayEffectExecutionDefinition& DamageInfo = DamageEffect->Executions[ExecutionIdx];

				const TSubclassOf<UGASCourseDamageExecution> DamageExecutionBPClass = LoadClass<UGASCourseDamageExecution>(GetTransientPackage(), TEXT("/Game/GASCourse/Game/Systems/Damage/DamageExecution_Base.DamageExecution_Base_C"));
				if (DamageExecutionBPClass->GetClass() != nullptr)
				{
					DamageInfo.CalculationClass = DamageExecutionBPClass;
				}
			
				int32 ModifiersIdx = DamageInfo.CalculationModifiers.Num();
				DamageInfo.CalculationModifiers.SetNum(ModifiersIdx + 2);
				FGameplayEffectExecutionScopedModifierInfo& DamageModifiers = DamageInfo.CalculationModifiers[ModifiersIdx];
				DamageModifiers.ModifierOp = EGameplayModOp::Additive;
			
				FSetByCallerFloat CallerFloat;
				CallerFloat.DataName = FName("");
				CallerFloat.DataTag = Data_IncomingDamage;
				DamageModifiers.ModifierMagnitude = FGameplayEffectModifierMagnitude(CallerFloat);
		
				DamageEffect->Executions[0].CalculationModifiers[0] = DamageModifiers;
				const FGameplayEffectSpecHandle DamageEffectHandle = MakeSpecHandle(DamageEffect, Instigator, Instigator, 1.0f);
				AssignTagSetByCallerMagnitude(DamageEffectHandle, Data_IncomingDamage, Damage);

				//TODO: Investigate how to add custom calculation class to damage application for randomization.
				/*
				FGameplayEffectExecutionScopedModifierInfo& DamageCalculationClass = DamageInfo.CalculationModifiers[++ModifiersIdx];
				DamageCalculationClass.ModifierOp = EGameplayModOp::Additive;
				*/
			
				FGameplayEffectContextHandle ContextHandle = GetEffectContext(DamageEffectHandle);
				if(DamageContext.HitResult.bBlockingHit)
				{
					ContextHandle.AddHitResult(DamageContext.HitResult);
				}
			
				AddGrantedTags(DamageEffectHandle, DamageContext.GrantedTags);
				AddGrantedTag(DamageEffectHandle, DamageContext.DamageType);
			
				InstigatorASC->ApplyGameplayEffectSpecToTarget(*DamageEffectHandle.Data.Get(), TargetASC);
				return true;
			}
		}
	}
	
	return false;
}

Finally, when I apply damage successfully, we pass along this status effect information to the Target Ability System component so that the burn stack effect can be applied:

GASCourseDamageExecution.cpp

TargetAbilitySystemComponent->ApplyGameplayStatusEffect(TargetAbilitySystemComponent, SourceAbilitySystemComponent, Spec.DynamicGrantedTags);

Now that we have a bit more context about how we are applying burn stacks onto a target when dealing fire damage, let’s dive deeper into the Burn status effect itself.


Burn Status Effect

The first status effect that I wanted to make was for Burning, and this effect has a few unique properties that make it interesting.

  • Burn status application requires a certain number of stacks before the actual burn status is applied.
  • Once Burn is applied, it needs to apply burn damage over time.
  • Like most status effects, we also want to support the idea of immunity where another gameplay effect can grant the character immunity to the status entirely.

Let’s start with Burn stacking:

Tranek: GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project.

We want to apply a burn stack Gameplay Effect every time we deal damage to an enemy with our projectile. Here is the Blueprint setup for the projectile damage and GE application:

As discussed above, we use the GASCourseStatusEffectTable to map the stack to the fire damage application pipeline. Here is how the Burn stack Gameplay Effect is setup:

The most important aspect of the setup is the Stacking section:

What this section is saying is that we want to monitor stacking on a per source basis, with Aggregate by Source. This decision is made so that each source actor applying a stack has to monitor the stacking themselves; in other words, each source needs to apply 5 stacks of burn before having a burn status applied. Alternatively, this can be monitored per Target, so that it doesn’t matter which source applies the burn stack and as long as 5 stacks are reached, the Burn status will be applied. The final choice in this regard is up to debate for my project.

Next, we enable Overflow which allows for a new Gameplay Effect to be applied when the number of stacks applied > than the Stack Limit Count; in this case we want to apply the GE_Burn_Full effect, which handles the actual Burn Status functionality. Please refer to the full burn stack GE image above for additional context on how other Gameplay Effect components are setup.

Now let’s talk about GE_Burn_Full, which is responsible for applying the Burn Status itself; here is what the Gameplay Effect setup looks like:


Here, we are granting and activating the ability, GA_Status_Burn, which is responisble for handling the damage over time:

We make sure that we apply the Asset Tag (Effect.AssetTag.Status.Burn) so that we can have our StatusEffectListener component class broadcast the application/removal of this Gameplay Effect in order to update our UI accordingly. This Asset Tag is also important for how we handle immunity to the Burn status, which I will show in a bit.

The final unique aspect of the Gameplay Effect is the GASCourseGameplayEffectUIData that I use to supply a soft referenced material icon that gets drawn above the NPC healthbar UI, as well as an Effect Descriptor class that we can use to supply verbose descriptions of the gameplay effect. We will be talking about that more later in this post.

Here is the final result in both multiplayer and single player contexts:

Burn_MP_GIF
Burn_SP_GIF


To end the section on the Burn status effect, I wanted to show-case an example of how Status Effect immunity can work. Here is what the GE_Burn_Immunity looks like:

Tranek: GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project.

The most important Gameplay Effect component is the Immunity Gameplay Effect Component which uses a tag query setup to know which Gameplay Effects to prevent application of while the Immunity effect is active. This is where the Asset Tag, and its hierarchy, is so important. Under Effect Tag Query, I add queries for both the stack and the burn status effect with Effect.AssetTag.Stack.Status.Burn | Effect.AssetTag.Status.Burn respectively. The power lies in being able to grant specific status immunities or groupings of statuses. For example, if GE_Burn_Immunity were to query only Effect.AssetTag.Status, then it would prevent *all statuses! Here is an example of the burn immunity in effect:

Burn_Immunity_GIF

Now that we have shown the Burn status, let’s move on to a simpler example with the Passive Healing effect.


Passive Healing Effect

The Passive Healing status is a much simpler concept than the Burn because it does not support stacking, and with its current setup, does not have any associated immunities; however, with its current setup, adding immunity would be super easy. Passive Healing allows for, as the name suggests, healing to take place passively over time. Here is the setup of GE_PassiveHealing:

The concept to be aware of is that this Gameplay Effect uses a Period of 0.1 seconds to reapply the attribute modification through that period time. For the time being, this effect is infinite but versions of the status effect can be made to last a certain duration.

The unique aspect of the Passive Healing setup is that the modification of the Health attribute takes place through the Gameplay Effect itself:

Here is the final result in both multiplayer and single player contexts:

PassiveHealing_MP_GIF
PassiveHealing_SP_GIF


Now let’s talk about the UI Data class and status descriptor.


GASCourseGameplayEffectUIData

By default, the Gameplay Ability System comes with a bare bones Gameplay Effect Component class called UGameplayEffectUIData and also a simple class that extends from this called GameplayEffectUIData_TextOnly:

GameplayEffectUIData_TextOnly.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameplayEffectUIData.h"
#include "GameplayEffectUIData_TextOnly.generated.h"

/**
 * UI data that contains only text. This is mostly used as an example of a subclass of UGameplayEffectUIData.
 * If your game needs only text, this is a reasonable class to use. To include more data, make a custom subclass of UGameplayEffectUIData.
 */
UCLASS()
class GAMEPLAYABILITIES_API UGameplayEffectUIData_TextOnly : public UGameplayEffectUIData
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Data, meta = (MultiLine = "true"))
	FText Description;
};

From UGameplayEffectUIData, I extended a new custom UIData component class called GASCourseGameplayEffectUIData which stores data related to how I wanted to show status effects onto my games’ UI. These include:

  • An icon representing the status effect.
  • A Blueprintable descriptor class that can be used to dynmically update the description of the status effect after its been applied.

Here is what this class looks like:

GASCourseGameplayEffectUIData.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameplayEffectUIData.h"
#include "Game/GameplayAbilitySystem/GameplayEffect/EffectDescriptor/GASCourseEffectDescriptor.h"
#include "GASCourseGameplayEffectUIData.generated.h"

/**
 * 
 */
UCLASS()
class GASCOURSE_API UGASCourseGameplayEffectUIData : public UGameplayEffectUIData
{
	GENERATED_BODY()

public:

	UGASCourseGameplayEffectUIData();

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Data)
	TSoftObjectPtr<UMaterialInterface> StatusIcon;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Data, meta = (MultiLine = "true"))
	FText StatusDescription;

	UFUNCTION(BlueprintCallable)
	FText ConstructStatusDescription(FActiveGameplayEffectHandle GameplayEffectHandle);

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Data)
	TSubclassOf<UGASCourseEffectDescriptor> EffectDescriptor;

	UFUNCTION(BlueprintCallable, meta = (WorldContext))
	UGASCourseEffectDescriptor* InitializeDescriptor(UObject* WorldContextObject);

private:

	UGASCourseEffectDescriptor* EffectDescriptorObj = nullptr;
	
};

GASCourseGameplayEffectUIData.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Game/GameplayAbilitySystem/GameplayEffect/GASCourseGameplayEffectUIData.h"

#include "AbilitySystemComponent.h"
#include "Game/GameplayAbilitySystem/GameplayEffect/EffectDescriptor/GASCourseEffectDescriptor.h"


UGASCourseGameplayEffectUIData::UGASCourseGameplayEffectUIData()
{

}

FText UGASCourseGameplayEffectUIData::ConstructStatusDescription(FActiveGameplayEffectHandle GameplayEffectHandle)
{
	if(EffectDescriptorObj)
	{
		StatusDescription = EffectDescriptorObj->GetEffectDescriptor(GameplayEffectHandle);
	}

	return StatusDescription;
}

UGASCourseEffectDescriptor* UGASCourseGameplayEffectUIData::InitializeDescriptor(UObject* WorldContextObject)
{
	if(EffectDescriptor)
	{
		EffectDescriptorObj = NewObject<UGASCourseEffectDescriptor>(WorldContextObject, EffectDescriptor);
		
		return EffectDescriptorObj;
	}

	return nullptr;
}

Now let’s dive into the GASCourseEffectDescriptor class.


GASCourseEffectDescriptor

This is the class that can be overriden in Blueprint to update the description of the Gameplay Effect in question. The root of where this data is written comes from the passed in FActiveGameplayEffectHandle GameplayEffectHandle. It is from this handle that the Blueprint can gather required data to add to the description. Here is the code:

GASCourseEffectDescriptor.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameplayEffect.h"
#include "GASCourseEffectDescriptor.generated.h"

/**
 * @class UGASCourseEffectDescriptor
 * @brief This class is responsible for defining the effect descriptor for a gameplay effect in GAS Course.
 *
 * The UGASCourseEffectDescriptor class is a subclass of UObject and provides the functionality for getting the effect descriptor of a gameplay effect.
 * It allows for blueprint implementations of the GetEffectDescriptor function to provide custom effect descriptor text based on the specified GameplayEffectHandle.
 */
UCLASS(Blueprintable)
class GASCOURSE_API UGASCourseEffectDescriptor : public UObject
{
	GENERATED_BODY()

public:

	UGASCourseEffectDescriptor();

	UFUNCTION(BlueprintNativeEvent)
	FText GetEffectDescriptor(FActiveGameplayEffectHandle GameplayEffectHandle);
	
	virtual UWorld* GetWorld() const override;

};

GASCourseEffectDescriptor.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Game/GameplayAbilitySystem/GameplayEffect/EffectDescriptor/GASCourseEffectDescriptor.h"

UGASCourseEffectDescriptor::UGASCourseEffectDescriptor()
{
}

FText UGASCourseEffectDescriptor::GetEffectDescriptor_Implementation(FActiveGameplayEffectHandle GameplayEffectHandle)
{
	FText Empty;
	return Empty;
}

UWorld* UGASCourseEffectDescriptor::GetWorld() const
{
	if(GetOuter() && !HasAnyFlags(RF_ClassDefaultObject))
	{
		return GetOuter()->GetWorld();
	}

	return nullptr;
}

Here are the example descriptors for Burn and Healing status effects:


These demonstrate, with some custom Blueprint functionality (this will be shown at the end of the post in the GASCourseASCBlueprintLibrary class), how we can query data from the Gameplay Effect Handle to construct the effect description!

BurnDescription_InGame
EffectDescription

Lastly, we need to integrate this into our status effect widget. Here is how it works:

On Event Construct of my status effect icon widget Blueprint, I get a reference to the UI data, update the status effect widget image icon, and initialize the description UObject using the widget as a world context object.

Here is how I load in the status effect icon and assign it to the icon brush element:

In the Designer tab of the widget Blueprint, under the Status Effect Icon Image details panel, specifically the Behavior section, I create a new bound function to update the tool tip text with a custom function (Get_StatusEffectIcon_ToolTipText) and setup a special cursor for when its being hovered

Lastly, we use the tool tip text bound function to construct our status description by calling Construct Status Description, passing in the handle we get from our StatusEffectListener component to get the data we need for the description.

To finish up this post, I will give you the code for my GASCourseASCBlueprintLibrary class that I use to handle damage and other helper functions to get data from Gameplay Effect handles. It is in my list of things to do to move damage related things to its own class or subsystem; but for now, it exists in the blueprint library class.


GASCourseASCBlueprintLibrary.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "AbilitySystemBlueprintLibrary.h"
#include "GameplayEffect.h"
#include "Game/GameplayAbilitySystem/GASCourseNativeGameplayTags.h"
#include "Game/GameplayAbilitySystem/GASCourseGameplayAbility.h"
#include "GASCourseASCBlueprintLibrary.generated.h"

/** Represents a context for applying damage to an object or character.
 *  Contains information related to the damage event such as the hit result,
 *  damage type, and additional gameplay tags associated with the damage.
 */

USTRUCT(blueprintable)
struct FDamageContext
{
	GENERATED_USTRUCT_BODY()

public:

	UPROPERTY(EditAnywhere, BlueprintReadWrite )
	FHitResult HitResult;

	UPROPERTY(EditAnywhere, BlueprintReadWrite , meta=(Categories="Damage.Type"))
	FGameplayTag DamageType = DamageType_Physical;

	UPROPERTY(EditAnywhere, BlueprintReadWrite )
	FGameplayTagContainer GrantedTags;
};

/**
 *  @struct FDamageOverTimeContext
 *  @brief Structure representing the context for damage over time.
 *
 *  Structure that holds the parameters necessary for applying damage over time.
 *
 *  @remark This structure is blueprintable.
 */
USTRUCT(blueprintable)
struct FDamageOverTimeContext
{
	GENERATED_USTRUCT_BODY()

public:

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float DamagePeriod = -1.0f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(Categories="Damage.Type"))
	float DamageDuration = -1.0f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite )
	bool bApplyDamageOnApplication = true;
};

/**
 * UGASCourseASCBlueprintLibrary is a blueprint library that provides utility functions for applying damage to target actors.
 */

UCLASS()
class GASCOURSE_API UGASCourseASCBlueprintLibrary : public UAbilitySystemBlueprintLibrary
{
	GENERATED_BODY()

public:
	
	/**
	 * Applies damage to the specified target actor using the specified instigator actor, damage amount, and damage context.
	 * This method is blueprint callable and can only be executed by an authority.
	 *
	 * @param Target The actor to apply the damage to.
	 * @param Instigator The actor initiating the damage.
	 * @param Damage The amount of damage to apply.
	 * @param DamageContext The context of the damage being applied.
	 * @return True if the damage was successfully applied, false otherwise.
	 */
	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GASCourse|AbilitySystem|Damage")
	static bool ApplyDamageToTarget(AActor* Target, AActor* Instigator, float Damage, const FDamageContext& DamageContext);

	/**
	 * Applies damage to multiple targets using the provided target data handle and damage context.
	 *
	 * @param TargetHandle - The gameplay ability target data handle representing the targets to apply damage to.
	 * @param Instigator - The actor that caused the damage.
	 * @param Damage - The amount of damage to apply.
	 * @param DamageContext - The context for applying the damage, containing information about the damage event.
	 *
	 * @return True if the damage was successfully applied to at least one target, false otherwise.
	 */
	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GASCourse|AbilitySystem|Damage")
	static bool ApplyDamageToTargetDataHandle(FGameplayAbilityTargetDataHandle TargetHandle, AActor* Instigator, float Damage, const FDamageContext& DamageContext);

	/**
	 * Applies damage over time to a target actor.
	 *
	 * @param Target                      The actor to apply damage over time to.
	 * @param Instigator                  The actor that caused the damage over time.
	 * @param Damage                      The amount of damage to apply over time.
	 * @param DamageContext               The context of the damage being applied.
	 * @param DamageOverTimeContext       The context of the damage over time being applied.
	 *
	 * @return                            Returns true if the damage over time was successfully applied, false otherwise.
	 */
	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GASCourse|AbilitySystem|Damage")
	static bool ApplyDamageOverTimeToTarget(AActor* Target, AActor* Instigator, float Damage, const FDamageContext& DamageContext, const FDamageOverTimeContext& DamageOverTimeContext);

	/**
	 * Applies physical damage to the specified target.
	 *
	 * @param Target    The actor to apply damage to.
	 * @param Instigator    The actor responsible for the damage.
	 * @param Damage    The amount of physical damage to apply.
	 * @param HitResult    The hit result information of the damage.
	 * @param DamageContext    The damage context information. Context is hidden and initialized in the function to pass DamageType_Physical by default.
	 * @return    True if the damage was successfully applied, false otherwise.
	 */

	UFUNCTION(BlueprintCallable, meta=(Hidepin = "DamageContext"), BlueprintAuthorityOnly, Category = "GASCourse|AbilitySystem|Damage")
	static bool ApplyPhysicalDamageToTarget(AActor* Target, AActor* Instigator, float Damage, const FHitResult& HitResult, FDamageContext& DamageContext);

	/**
	 * Apply fire damage to a target actor.
	 *
	 * @param Target The actor to apply the fire damage to.
	 * @param Instigator The actor that initiated the fire damage.
	 * @param Damage The amount of damage to apply.
	 * @param HitResult The hit result of the fire damage.
	 * @param DamageContext The damage context containing additional information about the fire damage. Context is hidden and initialized in the function to pass DamageType_Elemental_Fire by default.
	 * @param bApplyBurnStack Whether to apply a burn stack effect.
	 *
	 * @return True if the fire damage was successfully applied, false otherwise.
	 */
	UFUNCTION(BlueprintCallable, meta=(Hidepin = "DamageContext"), BlueprintAuthorityOnly, Category = "GASCourse|AbilitySystem|Damage")
	static bool ApplyFireDamageToTarget(AActor* Target, AActor* Instigator, float Damage, const FHitResult& HitResult, FDamageContext& DamageContext, bool bApplyBurnStack = true);

	/**
	 * Applies damage to a target actor using a gameplay effect.
	 *
	 * @param Target           The actor to apply damage to.
	 * @param Instigator       The actor causing the damage.
	 * @param Damage           The amount of damage to apply.
	 * @param DamageContext    The context of the damage.
	 * @param GameplayEffect   The gameplay effect to apply.
	 *
	 * @return True if the damage is successfully applied, false otherwise.
	 */
	static bool ApplyDamageToTarget_Internal(AActor* Target, AActor* Instigator, float Damage, const FDamageContext& DamageContext, UGameplayEffect* GameplayEffect);

	static UGameplayEffect* ConstructDamageGameplayEffect(EGameplayEffectDurationType DurationType, const FDamageOverTimeContext& DamageOverTimeContext);

	UFUNCTION(BlueprintPure, Category = "GASCourse|AbilitySystem|Damage")
	static bool FindDamageTypeTagInContainer(const FGameplayTagContainer& InContainer, FGameplayTag& DamageTypeTag);

	/**
	 * Retrieves the gameplay ability slot type from the specified ability spec handle.
	 *
	 * @param AbilitySystem The ability system component to retrieve the ability spec from.
	 * @param AbilitySpecHandle The handle of the ability spec to retrieve the slot type from.
	 * @return The gameplay ability slot type associated with the specified ability spec handle.
	 */
	UFUNCTION(BlueprintPure, Category = "GASCourse|AbilitySystem|GameplayAbility")
	static EGASCourseAbilitySlotType GetGameplayAbilitySlotTypeFromHandle(const UAbilitySystemComponent* AbilitySystem, const FGameplayAbilitySpecHandle& AbilitySpecHandle);

	UFUNCTION(BlueprintCallable, Category = "GASCourse|AbilitySystem|GameplayAbility")
	static void GetAllAbilitiesofAbilitySlotType(const UAbilitySystemComponent* AbilitySystem, EGASCourseAbilitySlotType AbilitySlot, TArray<FGameplayAbilitySpecHandle>& OutAbilityHandles);

	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "GASCourse|AbilitySystem|Damage")
	static void SendGameplayEventToTargetDataHandle(FGameplayAbilityTargetDataHandle TargetHandle, FGameplayTag EventTag, FGameplayEventData Payload);

	/**
	 * Retrieves the gameplay attribute from a given modifier struct.
	 *
	 * @param ModifierInfo The gameplay modifier info struct.
	 * @return The gameplay attribute.
	 */
	UFUNCTION(BlueprintCallable, Category = "GASCourse|AbilitySystem|GameplayEffect")
	static FGameplayAttribute GetGameplayAttributeFromModifierStruct(const FGameplayModifierInfo& ModifierInfo);

	/**
	 * Calculates the magnitude of a modifier for a given gameplay effect.
	 *
	 * @param InGameplayEffect The handle to the gameplay effect.
	 * @param ModifierIdx The index of the modifier to calculate the magnitude for.
	 * @param bFactorInStackCount Specifies whether to factor in the stack count of the gameplay effect.
	 *
	 * @return The magnitude of the specified modifier. If the modifier is not found or the gameplay effect is invalid,
	 *         returns 0.0f.
	 */
	UFUNCTION(BlueprintCallable, Category = "GASCourse|AbilitySystem|GameplayEffect")
	static float GetModifierMagnitudeAtIndex(FActiveGameplayEffectHandle InGameplayEffect, int32 ModifierIdx, bool bFactorInStackCount);

	/**
	 * Retrieves the gameplay effect specification handle associated with the given active gameplay effect handle.
	 *
	 * @param InGameplayEffect The active gameplay effect handle for which to retrieve the gameplay effect specification handle.
	 * @return The gameplay effect specification handle associated with the given active gameplay effect handle.
	 */
	UFUNCTION(BlueprintCallable, Category = "GASCourse|AbilitySystem|GameplayEffect")
	static FGameplayEffectSpec GetSpecHandleFromGameplayEffect(FActiveGameplayEffectHandle InGameplayEffect);

	/**
	 * Retrieves the period of a gameplay effect.
	 *
	 * @param InGameplayEffect The handle to the active gameplay effect.
	 * @return The period of the gameplay effect.
	 */
	UFUNCTION(BlueprintPure, Category =  "GASCourse|AbilitySystem|GameplayEffect")
	static float GetPeriodFromGameplayEffect(FActiveGameplayEffectHandle InGameplayEffect);
};

GASCourseASCBlueprintLibrary.cpp


// Fill out your copyright notice in the Description page of Project Settings.


#include "Game/BlueprintLibraries/GameplayAbilitySystem/GASCourseASCBlueprintLibrary.h"
#include "Game/GameplayAbilitySystem/GASCourseAbilitySystemComponent.h"
#include "Game/Systems/Damage/GASCourseDamageExecution.h"
#include "Game/GameplayAbilitySystem/GASCourseNativeGameplayTags.h"
#include "Game/GameplayAbilitySystem/GASCourseGameplayEffect.h"

bool UGASCourseASCBlueprintLibrary::ApplyDamageToTarget(AActor* Target, AActor* Instigator, float Damage, const FDamageContext& DamageContext)
{
	//Initialize DoTContext to default values to make damage instant.
	constexpr FDamageOverTimeContext DamageOverTimeContext;
	UGameplayEffect* DamageEffect = ConstructDamageGameplayEffect(EGameplayEffectDurationType::Instant, DamageOverTimeContext);
	return ApplyDamageToTarget_Internal(Target, Instigator, Damage, DamageContext, DamageEffect);
}

bool UGASCourseASCBlueprintLibrary::ApplyDamageToTargetDataHandle(FGameplayAbilityTargetDataHandle TargetHandle,
	AActor* Instigator, float Damage, const FDamageContext& DamageContext)
{
	TArray<AActor*> Targets = GetAllActorsFromTargetData(TargetHandle);
	bool bDamageApplied = false;
	
	for(AActor* Target: Targets)
	{
		bDamageApplied = ApplyDamageToTarget(Target, Instigator, Damage, DamageContext);
	}
	return bDamageApplied;
}

bool UGASCourseASCBlueprintLibrary::ApplyDamageOverTimeToTarget(AActor* Target, AActor* Instigator, float Damage,
                                                                const FDamageContext& DamageContext, const FDamageOverTimeContext& DamageOverTimeContext)
{
	UGameplayEffect* DamageEffect = ConstructDamageGameplayEffect(EGameplayEffectDurationType::HasDuration, DamageOverTimeContext);
	return ApplyDamageToTarget_Internal(Target, Instigator, Damage, DamageContext, DamageEffect);
}

bool UGASCourseASCBlueprintLibrary::ApplyPhysicalDamageToTarget(AActor* Target, AActor* Instigator, float Damage,
                                                                const FHitResult& HitResult, FDamageContext& DamageContext)
{
	DamageContext.DamageType = DamageType_Physical;
	DamageContext.HitResult = HitResult;
	constexpr FDamageOverTimeContext DamageOverTimeContext;
	UGameplayEffect* DamageEffect = ConstructDamageGameplayEffect(EGameplayEffectDurationType::Instant, DamageOverTimeContext);
	return ApplyDamageToTarget_Internal(Target, Instigator, Damage, DamageContext, DamageEffect);
}

bool UGASCourseASCBlueprintLibrary::ApplyFireDamageToTarget(AActor* Target, AActor* Instigator, float Damage,
                                                            const FHitResult& HitResult, FDamageContext& DamageContext, bool bApplyBurnStack)
{
	DamageContext.DamageType = DamageType_Elemental_Fire;
	if(bApplyBurnStack)
	{
		FGameplayTagContainer GrantedTags;
		GrantedTags.AddTag(FGameplayTag::RequestGameplayTag(FName("Effect.Gameplay.Status.Burn.Stack")));
		DamageContext.GrantedTags = GrantedTags;
	}
	DamageContext.HitResult = HitResult;
	
	constexpr FDamageOverTimeContext DamageOverTimeContext;
	UGameplayEffect* DamageEffect = ConstructDamageGameplayEffect(EGameplayEffectDurationType::Instant, DamageOverTimeContext);
	
	return ApplyDamageToTarget_Internal(Target, Instigator, Damage, DamageContext, DamageEffect);
}

bool UGASCourseASCBlueprintLibrary::ApplyDamageToTarget_Internal(AActor* Target, AActor* Instigator, float Damage,
                                                                 const FDamageContext& DamageContext, UGameplayEffect* GameplayEffect)
{
	if(!Instigator && !Target)
	{
		return false;
	}

	//TODO: Add check to verify ability system component + consider damage/health interface for Non-GAS actors
	if(UGASCourseAbilitySystemComponent* TargetASC = Target->GetComponentByClass<UGASCourseAbilitySystemComponent>())
	{
		if(UGASCourseAbilitySystemComponent* InstigatorASC = Instigator->GetComponentByClass<UGASCourseAbilitySystemComponent>())
		{
			if(UGASCourseGameplayEffect* DamageEffect = Cast<UGASCourseGameplayEffect>(GameplayEffect))
			{
							
				const int32 ExecutionIdx = DamageEffect->Executions.Num();
				DamageEffect->Executions.SetNum(ExecutionIdx + 1);
				FGameplayEffectExecutionDefinition& DamageInfo = DamageEffect->Executions[ExecutionIdx];

				const TSubclassOf<UGASCourseDamageExecution> DamageExecutionBPClass = LoadClass<UGASCourseDamageExecution>(GetTransientPackage(), TEXT("/Game/GASCourse/Game/Systems/Damage/DamageExecution_Base.DamageExecution_Base_C"));
				if (DamageExecutionBPClass->GetClass() != nullptr)
				{
					DamageInfo.CalculationClass = DamageExecutionBPClass;
				}
			
				int32 ModifiersIdx = DamageInfo.CalculationModifiers.Num();
				DamageInfo.CalculationModifiers.SetNum(ModifiersIdx + 2);
				FGameplayEffectExecutionScopedModifierInfo& DamageModifiers = DamageInfo.CalculationModifiers[ModifiersIdx];
				DamageModifiers.ModifierOp = EGameplayModOp::Additive;
			
				FSetByCallerFloat CallerFloat;
				CallerFloat.DataName = FName("");
				CallerFloat.DataTag = Data_IncomingDamage;
				DamageModifiers.ModifierMagnitude = FGameplayEffectModifierMagnitude(CallerFloat);
		
				DamageEffect->Executions[0].CalculationModifiers[0] = DamageModifiers;
				const FGameplayEffectSpecHandle DamageEffectHandle = MakeSpecHandle(DamageEffect, Instigator, Instigator, 1.0f);
				AssignTagSetByCallerMagnitude(DamageEffectHandle, Data_IncomingDamage, Damage);

				//TODO: Investigate how to add custom calculation class to damage application for randomization.
				/*
				FGameplayEffectExecutionScopedModifierInfo& DamageCalculationClass = DamageInfo.CalculationModifiers[++ModifiersIdx];
				DamageCalculationClass.ModifierOp = EGameplayModOp::Additive;
				*/
			
				FGameplayEffectContextHandle ContextHandle = GetEffectContext(DamageEffectHandle);
				if(DamageContext.HitResult.bBlockingHit)
				{
					ContextHandle.AddHitResult(DamageContext.HitResult);
				}
			
				AddGrantedTags(DamageEffectHandle, DamageContext.GrantedTags);
				AddGrantedTag(DamageEffectHandle, DamageContext.DamageType);
			
				InstigatorASC->ApplyGameplayEffectSpecToTarget(*DamageEffectHandle.Data.Get(), TargetASC);
				return true;
			}
		}
	}
	
	return false;
}

UGameplayEffect* UGASCourseASCBlueprintLibrary::ConstructDamageGameplayEffect(EGameplayEffectDurationType DurationType,  const FDamageOverTimeContext& DamageOverTimeContext)
{
	UGASCourseGameplayEffect* DamageEffect = NewObject<UGASCourseGameplayEffect>(GetTransientPackage(), FName(TEXT("Damage")));
	if(DurationType == EGameplayEffectDurationType::Instant)
	{
		DamageEffect->DurationPolicy = EGameplayEffectDurationType::Instant;
	}
	else
	{
		DamageEffect->DurationPolicy = EGameplayEffectDurationType::HasDuration;
				
		//DamageOverTimeContext should specify FScalableFloat for duration parameter.
		FScalableFloat Duration;
		Duration.Value = DamageOverTimeContext.DamageDuration;
		DamageEffect->DurationMagnitude = FGameplayEffectModifierMagnitude(Duration);

		//DamageOverTimeContext should specify FScalableFloat for period parameter.
		FScalableFloat Period;
		Period.Value = DamageOverTimeContext.DamagePeriod;
		DamageEffect->Period = Period;
		DamageEffect->bExecutePeriodicEffectOnApplication = DamageOverTimeContext.bApplyDamageOnApplication;
	}
	
	return DamageEffect;
}

bool UGASCourseASCBlueprintLibrary::FindDamageTypeTagInContainer(const FGameplayTagContainer& InContainer, FGameplayTag& DamageTypeTag)
{
	if(InContainer.HasTag(FGameplayTag::RequestGameplayTag(FName("Damage.Type"))))
	{
		for(FGameplayTag Tag : InContainer.GetGameplayTagArray())
		{
			if(Tag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("Damage.Type"))))
			{
				DamageTypeTag = Tag;
				return true;
			}
		}
	}
	
	return false;
}

EGASCourseAbilitySlotType UGASCourseASCBlueprintLibrary::GetGameplayAbilitySlotTypeFromHandle(
	const UAbilitySystemComponent* AbilitySystem, const FGameplayAbilitySpecHandle& AbilitySpecHandle)
{
	EGASCourseAbilitySlotType AbilitySlot = EGASCourseAbilitySlotType::EmptySlot;
	// validate the ASC
	if (!AbilitySystem)
	{
		return AbilitySlot;
	}

	// get and validate the ability spec
	const FGameplayAbilitySpec* AbilitySpec = AbilitySystem->FindAbilitySpecFromHandle(AbilitySpecHandle);
	if (!AbilitySpec)
	{
		return AbilitySlot;
	}

	// try to get the ability instance
	if(const UGASCourseGameplayAbility* AbilityInstance = Cast<UGASCourseGameplayAbility>(AbilitySpec->GetPrimaryInstance()))
	{
		AbilitySlot = AbilityInstance->GetAbilitySlotType();
	}

	return AbilitySlot;
}

void UGASCourseASCBlueprintLibrary::GetAllAbilitiesofAbilitySlotType(const UAbilitySystemComponent* AbilitySystem,  EGASCourseAbilitySlotType AbilitySlot, 
	TArray<FGameplayAbilitySpecHandle>& OutAbilityHandles)
{
	if(AbilitySystem)
	{
		OutAbilityHandles.Empty(AbilitySystem->GetActivatableAbilities().Num());
		for (const FGameplayAbilitySpec& Spec : AbilitySystem->GetActivatableAbilities())
		{
			if(GetGameplayAbilitySlotTypeFromHandle(AbilitySystem, Spec.Handle) == AbilitySlot)
			{
				// add the spec handle to the list
				OutAbilityHandles.Add(Spec.Handle);
			}
		}
	}
}

void UGASCourseASCBlueprintLibrary::SendGameplayEventToTargetDataHandle(FGameplayAbilityTargetDataHandle TargetHandle,
	FGameplayTag EventTag, FGameplayEventData Payload)
{
	TArray<AActor*> Targets = GetAllActorsFromTargetData(TargetHandle);
	for(AActor* Target : Targets)
	{
		SendGameplayEventToActor(Target, EventTag, Payload);
	}
}

FGameplayAttribute UGASCourseASCBlueprintLibrary::GetGameplayAttributeFromModifierStruct(
	const FGameplayModifierInfo& ModifierInfo)
{
	FGameplayAttribute Attribute;

	if(ModifierInfo.Attribute.IsValid())
	{
		Attribute = ModifierInfo.Attribute;
	}

	return Attribute;
}

float UGASCourseASCBlueprintLibrary::GetModifierMagnitudeAtIndex(FActiveGameplayEffectHandle InGameplayEffect, int32 ModifierIdx,
	bool bFactorInStackCount)
{
	float OutModifierMagnitude = 0.0f;
	
	const FGameplayEffectSpec& Spec = GetSpecHandleFromGameplayEffect(InGameplayEffect);
	if(Spec.Def)
	{
		Spec.Def->Modifiers[ModifierIdx].ModifierMagnitude.AttemptCalculateMagnitude(Spec, OutModifierMagnitude);
	}
	
	return OutModifierMagnitude;
}

FGameplayEffectSpec UGASCourseASCBlueprintLibrary::GetSpecHandleFromGameplayEffect(FActiveGameplayEffectHandle InGameplayEffect)
{
	FGameplayEffectSpec OutSpec;
	if(const UAbilitySystemComponent* AbilitySystemComponent = InGameplayEffect.GetOwningAbilitySystemComponent())
	{
		if(const FActiveGameplayEffect* ActiveGameplayEffect = AbilitySystemComponent->GetActiveGameplayEffect(InGameplayEffect))
		{
			OutSpec = ActiveGameplayEffect->Spec;
		}
	}

	return OutSpec;
}

float UGASCourseASCBlueprintLibrary::GetPeriodFromGameplayEffect(FActiveGameplayEffectHandle InGameplayEffect)
{
	float OutPeriod = 0.0f;

	const FGameplayEffectSpec& Spec = GetSpecHandleFromGameplayEffect(InGameplayEffect);
	if(Spec.Def)
	{
		OutPeriod = Spec.GetPeriod();
	}
	
	return OutPeriod;
}

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 :slight_smile:


Next Blog Post Topic:

*Moving NPC Reaction Ability Logic to State Tree

2 Likes