Hello again everyone! Welcome to another entry in this on-going blog/development thread for my GAS Course Project. For more context of this project, please see the very first post in this thread, and as usual, you can find my socials here:
Twitch : Twitch
Discord : UE5 - GAS Course Project - JevinsCherries
Tiktok : Devin Sherry (@jevinscherriesgamedev) | TikTok
Youtube : Youtube
Twitter : https://twitter.com/jevincherries
Instagram : Jevins Cherries Gamedev (@jevinscherriesgamedev) • Instagram photos and videos
Today we are going to talk about my implementation of critical chance and critical damage attributes, and how I use them to calculate critical hits as well as show this information as part of my damage pipeline. This implementation also takes into consideration critical chance roll preservation for damage over time effects so that critical chance is only taken into account on the initial damage application and applied to consecutive damage executions.
Classes to Research:
UGameplayEffectExecutionCalculation: Gameplay Effect Execution Calculation – Blueprint Attributes
Additional Reading:
The goal of critical chance and critical damage is to add a layer of player agency when inflicting damage onto enemies. Players can craft a build around increasing critical chance, modifying their critical damage output, and widen how critical chance can be taken into consideration. When it comes to my implementation, I wanted to make sure that critical chance/damage is not calculated every damage tick in DoT effects, so this is the main constraint that I placed on myself on its design. Let’s start with the creation of the critical chance and critical damage multiplier attributes inside of my UGASCourseHealthAttributeSet
:
UPROPERTY(BlueprintReadOnly, Category = "Damage", ReplicatedUsing=OnRep_CriticalChance)
FGameplayAttributeData CriticalChance = 0.0f;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, CriticalChance)
UPROPERTY(BlueprintReadOnly, Category = "Damage", ReplicatedUsing=OnRep_CriticalDamageMultiplier)
FGameplayAttributeData CriticalDamageMultiplier = 0.0f;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, CriticalDamageMultiplier)
Here is the full code snippet for the attribute set:
UGASCourseHealthAttributeSet.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "Game/GameplayAbilitySystem/AttributeSets/GASCourseAttributeSet.h"
#include "GASCourseHealthAttributeSet.generated.h"
// Uses macros from AttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
/**
* @class UGASCourseHealthAttributeSet
* @brief This class represents a specialized Attribute Set for managing health-related attributes in a Gameplay Ability System (GAS).
*
* UGASCourseHealthAttributeSet is designed to encapsulate attributes crucial to gameplay```cpp mechanics
/**
,
* such as * @ health, allowing for a modularclass UGASCourseHealthAttribute and scalable approach to handling player or characterSet
* @brief Represents stats.
* It interacts with the attribute set containing health-related the Unreal Engine's attributes for Ability System the gameplay ability system.
*
* This attribute set is designed to manage and encapsulate gameplay Component and-related health supports automatic synchronization
* of attributes.
attribute * changes It integrates with across clients Unreal Engine and servers's Gameplay.
*
Ability System (GAS) to handle health computations * This,
* class defines such as the core current health, max set of health, health-related and other attributes utilized derived or modified health in the course, properties.
*
such as * The attribute CurrentHealth set typically responds to
* and Max changes in theseHealth properties., propagates modifications,
* It is and can trigger events primarily intended or gameplay to be used in conjunction with Unreal logic based Engine's on specific GAS framework thresholds or events.
,
* *
* enabling energy Subclass-efficient and this attribute event-driven set to gameplay systems add custom.
*
health-related * The attributes or Attribute expand functionalitySet is used.
*
* Attributes within the in this Ability System, where attributes like health can set are generally replicated and synchronized with the server, enabling
be modified * consistent temporarily
gameplay state across network * ored multiplayer permanently, environments. and changes Modification of are propagated attributes
according to GAS rules * should be handled.
*
using the * Key GAS's Features:
prediction system * - to ensure Health management robustness and accuracy during gameplay.
for characters *
* This class is designed to be, including integrated into current and Unreal Engine maximum health's AttributeSet data values.
model
* - * and should be Automatic replication used within of critical the context attributes via of GAS Unreal Engine-based gameplay mechanics.
*/
UCLASS()
class UGASCourseHealthAttributeSet : public UGASCourseAttributeSet
{
GENERATED_BODY()
public:
UGASCourseHealthAttributeSet();
// AttributeSet Overrides
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
virtual void PostAttributeBaseChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) const override;
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
public:
UPROPERTY(BlueprintReadOnly, Category = "Character Health Attributes", ReplicatedUsing=OnRep_CurrentHealth)
FGameplayAttributeData CurrentHealth;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, CurrentHealth)
UPROPERTY(BlueprintReadOnly, Category = "Character Health Attributes", ReplicatedUsing=OnRep_MaxHealth)
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, MaxHealth)
UPROPERTY(BlueprintReadOnly, Category = "Character Health Attributes", ReplicatedUsing=OnRep_StatusDamageHealingCoefficient)
FGameplayAttributeData StatusDamageHealingCoefficient;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, StatusDamageHealingCoefficient)
UPROPERTY(BlueprintReadOnly, Category = "Character Health Attributes", ReplicatedUsing=OnRep_ElementalDamageHealingCoefficient)
FGameplayAttributeData ElementalDamageHealingCoefficient;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, ElementalDamageHealingCoefficient)
UPROPERTY(BlueprintReadOnly, Category = "Character Health Attributes", ReplicatedUsing=OnRep_PhysicalDamageHealingCoefficient)
FGameplayAttributeData PhysicalDamageHealingCoefficient;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, PhysicalDamageHealingCoefficient)
UPROPERTY(BlueprintReadOnly, Category = "Character Health Attributes", ReplicatedUsing=OnRep_AllDamageHealingCoefficient)
FGameplayAttributeData AllDamageHealingCoefficient;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, AllDamageHealingCoefficient)
UPROPERTY(BlueprintReadOnly, Category = "Damage")
FGameplayAttributeData IncomingDamage;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, IncomingDamage)
UPROPERTY(BlueprintReadOnly, Category = "Damage", ReplicatedUsing=OnRep_CriticalChance)
FGameplayAttributeData CriticalChance = 0.0f;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, CriticalChance)
UPROPERTY(BlueprintReadOnly, Category = "Damage", ReplicatedUsing=OnRep_CriticalDamageMultiplier)
FGameplayAttributeData CriticalDamageMultiplier = 0.0f;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, CriticalDamageMultiplier)
UPROPERTY(BlueprintReadOnly, Category = "Healing")
FGameplayAttributeData IncomingHealing;
ATTRIBUTE_ACCESSORS(UGASCourseHealthAttributeSet, IncomingHealing)
protected:
UFUNCTION()
virtual void OnRep_CurrentHealth(const FGameplayAttributeData& OldCurrentHealth);
UFUNCTION()
virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth);
UFUNCTION()
virtual void OnRep_StatusDamageHealingCoefficient(const FGameplayAttributeData& OldStatusDamageHealingCoefficient);
UFUNCTION()
virtual void OnRep_ElementalDamageHealingCoefficient(const FGameplayAttributeData& OldElementalDamageHealingCoefficient);
UFUNCTION()
virtual void OnRep_PhysicalDamageHealingCoefficient(const FGameplayAttributeData& OldPhysicalDamageHealingCoefficient);
UFUNCTION()
virtual void OnRep_AllDamageHealingCoefficient(const FGameplayAttributeData& OldAllDamageHealingCoefficient);
UFUNCTION()
virtual void OnRep_CriticalChance(const FGameplayAttributeData& OldCriticalChance);
UFUNCTION()
virtual void OnRep_CriticalDamageMultiplier(const FGameplayAttributeData& OldCriticalDamageMultiplier);
};
UGASCourseHealthAttributeSet.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "Game/GameplayAbilitySystem/AttributeSets/GASCourseHealthAttributeSet.h"
#include "Game/GameplayAbilitySystem/GASCourseNativeGameplayTags.h"
#include "GameplayEffectExtension.h"
#include "Game/GameplayAbilitySystem/GASCourseGameplayEffect.h"
#include "GASCourse/GASCourseCharacter.h"
#include "Net/UnrealNetwork.h"
UGASCourseHealthAttributeSet::UGASCourseHealthAttributeSet()
{
}
void UGASCourseHealthAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
if(Attribute == GetMaxHealthAttribute())
{
AdjustAttributeForMaxChange(CurrentHealth, MaxHealth, NewValue, GetCurrentHealthAttribute());
}
if(Attribute == GetCurrentHealthAttribute())
{
NewValue = FMath::Clamp<float>(NewValue, 0.0f, MaxHealth.GetCurrentValue());
}
if(Attribute == GetStatusDamageHealingCoefficientAttribute())
{
NewValue = FMath::Clamp<float>(NewValue, 0.0f, 1.0f);
}
if(Attribute == GetAllDamageHealingCoefficientAttribute())
{
NewValue = FMath::Clamp<float>(NewValue, 0.0f, 1.0f);
}
if(Attribute == GetElementalDamageHealingCoefficientAttribute())
{
NewValue = FMath::Clamp<float>(NewValue, 0.0f, 1.0f);
}
if(Attribute == GetPhysicalDamageHealingCoefficientAttribute())
{
NewValue = FMath::Clamp<float>(NewValue, 0.0f, 1.0f);
}
if(Attribute == GetCriticalChanceAttribute())
{
NewValue = FMath::Clamp<float>(NewValue, 0.0f, 1.0f);
}
if(Attribute == GetCriticalDamageMultiplierAttribute())
{
NewValue = FMath::Clamp<float>(NewValue, 0.0f, 10.0f);
}
}
void UGASCourseHealthAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue,
float NewValue)
{
Super::PostAttributeChange(Attribute, OldValue, NewValue);
}
void UGASCourseHealthAttributeSet::PostAttributeBaseChange(const FGameplayAttribute& Attribute, float OldValue,
float NewValue) const
{
Super::PostAttributeBaseChange(Attribute, OldValue, NewValue);
if(Attribute == GetCurrentHealthAttribute())
{
NewValue = FMath::Clamp<float>(NewValue, 0.0f, MaxHealth.GetCurrentValue());
}
}
void UGASCourseHealthAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// Get the Target actor, which should be our owner
AActor* TargetActor = nullptr;
AController* TargetController = nullptr;
AGASCourseCharacter* TargetCharacter = nullptr;
if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
{
TargetActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
TargetCharacter = Cast<AGASCourseCharacter>(TargetActor);
}
if(Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalDamage = GetIncomingDamage();
SetIncomingDamage(0.0f);
bool bIsAlive = TargetCharacter->IsCharacterAlive();
const float HealthBeforeDamage = CurrentHealth.GetCurrentValue();
const float NewHealth = CurrentHealth.GetCurrentValue() - LocalDamage;
SetCurrentHealth(FMath::Clamp(NewHealth, 0.0f, GetMaxHealth()));
if(NewHealth <= 0.0f && bIsAlive)
{
FGameplayEventData OnDeathPayload;
OnDeathPayload.EventTag = Event_OnDeath;
OnDeathPayload.Instigator = Data.EffectSpec.GetContext().GetOriginalInstigator();
OnDeathPayload.Target = GetOwningActor();
OnDeathPayload.ContextHandle = Data.EffectSpec.GetContext();
OnDeathPayload.EventMagnitude = LocalDamage;
GetOwningAbilitySystemComponent()->HandleGameplayEvent(Event_OnDeath, &OnDeathPayload);
}
}
//Passive Healing Event
if(Data.EvaluatedData.Attribute == GetIncomingHealingAttribute() && CurrentHealth.GetCurrentValue() != MaxHealth.GetCurrentValue())
{
const float LocalIncomingHealing = GetIncomingHealing();
SetIncomingHealing(0.0f);
float NewCurrentHealth = GetCurrentHealth() + LocalIncomingHealing;
SetCurrentHealth(NewCurrentHealth);
FGameplayEventData OnHealingPayload;
OnHealingPayload.EventTag = Event_Gameplay_OnHealing;
OnHealingPayload.Instigator = Data.EffectSpec.GetContext().GetOriginalInstigator();
OnHealingPayload.Target = GetOwningActor();
OnHealingPayload.ContextHandle = Data.EffectSpec.GetContext();
OnHealingPayload.EventMagnitude = LocalIncomingHealing;
GetOwningAbilitySystemComponent()->HandleGameplayEvent(Event_Gameplay_OnHealing, &OnHealingPayload);
}
}
void UGASCourseHealthAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UGASCourseHealthAttributeSet, CurrentHealth, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASCourseHealthAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASCourseHealthAttributeSet, StatusDamageHealingCoefficient, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASCourseHealthAttributeSet, ElementalDamageHealingCoefficient, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASCourseHealthAttributeSet, PhysicalDamageHealingCoefficient, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASCourseHealthAttributeSet, AllDamageHealingCoefficient, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASCourseHealthAttributeSet, CriticalChance, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UGASCourseHealthAttributeSet, CriticalDamageMultiplier, COND_None, REPNOTIFY_Always);
}
void UGASCourseHealthAttributeSet::OnRep_CurrentHealth(const FGameplayAttributeData& OldCurrentHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCourseHealthAttributeSet, CurrentHealth, OldCurrentHealth);
}
void UGASCourseHealthAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCourseHealthAttributeSet, MaxHealth, OldMaxHealth);
}
void UGASCourseHealthAttributeSet::OnRep_StatusDamageHealingCoefficient(const FGameplayAttributeData& OldDamageOverTimeHealingCoefficient)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCourseHealthAttributeSet, StatusDamageHealingCoefficient, OldDamageOverTimeHealingCoefficient);
}
void UGASCourseHealthAttributeSet::OnRep_ElementalDamageHealingCoefficient(
const FGameplayAttributeData& OldElementalDamageHealingCoefficient)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCourseHealthAttributeSet, ElementalDamageHealingCoefficient, OldElementalDamageHealingCoefficient);
}
void UGASCourseHealthAttributeSet::OnRep_PhysicalDamageHealingCoefficient(
const FGameplayAttributeData& OldPhysicalDamageHealingCoefficient)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCourseHealthAttributeSet, PhysicalDamageHealingCoefficient, OldPhysicalDamageHealingCoefficient);
}
void UGASCourseHealthAttributeSet::OnRep_AllDamageHealingCoefficient(
const FGameplayAttributeData& OldAllDamageHealingCoefficient)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCourseHealthAttributeSet, AllDamageHealingCoefficient, OldAllDamageHealingCoefficient);
}
void UGASCourseHealthAttributeSet::OnRep_CriticalChance(const FGameplayAttributeData& OldCriticalChance)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCourseHealthAttributeSet, CriticalChance, OldCriticalChance);
}
void UGASCourseHealthAttributeSet::OnRep_CriticalDamageMultiplier(
const FGameplayAttributeData& OldCriticalDamageMultiplier)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCourseHealthAttributeSet, CriticalDamageMultiplier, OldCriticalDamageMultiplier);
}
Now that I have my attributes declared, let’s take them into consideration in my damage execution calculation class. To start, we need to capture these attributes from the source actor of the damage:
struct GASCourseDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(IncomingDamage);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalDamageMultiplier);
GASCourseDamageStatics()
{
DEFINE_ATTRIBUTE_CAPTUREDEF(UGASCourseHealthAttributeSet, IncomingDamage, Source, true);
DEFINE_ATTRIBUTE_CAPTUREDEF(UGASCourseHealthAttributeSet, CriticalChance, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UGASCourseHealthAttributeSet, CriticalDamageMultiplier, Source, false);
}
};
UGASCourseDamageExecution::UGASCourseDamageExecution()
{
RelevantAttributesToCapture.Add(DamageStatics().IncomingDamageDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalChanceDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalDamageMultiplierDef);
}
Then, we use a gameplay tag Data_DamageOverTime
to label incoming damage that is part of a DoT effect so that we can determine if we need to re-roll critical chance and recalculate the critical damage output. This DoT tag comes as part of the payload when applying the damage gameplay effect, which we will talk a bit later on.
We take advantage of theSetSetByCallerMagnitude
to map the cached damage to the tag Data_CachedDamage so that we can read the value when re-applying damage in the cases of DoT effects. When we apply single damage effects, we bypass this check entirely. This gives us the ability to only roll critical chance once when a DoT effect is applied, and use that result in the consecutive damage ticks of the effect. This means that if the initial DoT application passes critical hit, all damage ticks are critical, instead of having to re-roll each time!
bool bUsingCachedDamage = false;
bool bCriticalHit = false;
float Damage = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().IncomingDamageDef, EvaluationParameters, Damage);
// Add SetByCaller damage if it exists
Damage += FMath::Max<float>(Spec->GetSetByCallerMagnitude(Data_IncomingDamage, false, -1.0f), 0.0f);
if (Spec->DynamicGrantedTags.HasTagExact(Data_DamageOverTime))
{
UE_LOG(LogTemp, Warning, TEXT("Damage Over Time"));
float CachedDamage = Spec->GetSetByCallerMagnitude(Data_CachedDamage);
if (CachedDamage > 0.0f)
{
Damage = CachedDamage;
UE_LOG(LogTemp, Warning, TEXT("Using Cached Damage"));
// Set the Target's damage meta attribute
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().IncomingDamageProperty, EGameplayModOp::Additive, Damage));
bUsingCachedDamage = true;
}
}
if (bUsingCachedDamage == false)
{
/*
* Critical Chance + Critical Damage
*/
float CriticalChance = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalChanceDef, EvaluationParameters, CriticalChance);
float CriticalDamageMultiplier = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalDamageMultiplierDef, EvaluationParameters, CriticalDamageMultiplier);
float RolledChancePercentage = FMath::RandRange(0.0f, 1.0f);
bCriticalHit = RolledChancePercentage <= CriticalChance;
if (bCriticalHit)
{
Damage += FMath::Floor(Damage * CriticalDamageMultiplier);
}
if (Damage > 0.f)
{
// Set the Target's damage meta attribute
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().IncomingDamageProperty, EGameplayModOp::Additive, Damage));
}
//Store damage as cached damage
Spec->SetSetByCallerMagnitude(Data_CachedDamage, Damage);
}
We also make sure to pass along the fact that the damage was critical as part of the damage payload so that we can read this later on when displaying the information to the HUD:
if (bCriticalHit)
{
DamageDealtPayload.InstigatorTags.AddTag(DamageType_Critical);
}
Here is the full UGASCourseDamageExecution
class:
UGASCourseDamageExecution.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "GameplayEffectExecutionCalculation.h"
#include "GASCourseDamageExecution.generated.h"
/**
* \brief Represents the custom execution calculation for damage in the gameplay ability system.
*
* This class provides functionality to calculate and apply damage during the execution of a gameplay effect.
* It captures relevant attributes necessary for damage computation and processes damage application, taking
* into account modifiers, gameplay tags, and any other specified parameters.
*/
UCLASS()
class UGASCourseDamageExecution : public UGameplayEffectExecutionCalculation
{
GENERATED_BODY()
public:
UGASCourseDamageExecution();
/** \brief Executes the custom gameplay effect implementation.
*
* This method is invoked when a gameplay effect with a custom execution is applied. It calculates and applies the damage to the target actor,
* taking into account any damage modifiers and tags. It also broadcasts the damage dealt event to the target and source ability system components.
*
* \param ExecutionParams The parameters for the execution of the gameplay effect.
* \param OutExecutionOutput The output data of the execution of the gameplay effect.
*/
virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
};
UGASCourseDamageExecution.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "Game/Systems/Damage/GASCourseDamageExecution.h"
#include "Game/GameplayAbilitySystem/AttributeSets/GASCourseHealthAttributeSet.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemGlobals.h"
#include "Game/GameplayAbilitySystem/GASCourseAbilitySystemComponent.h"
#include "Game/GameplayAbilitySystem/GASCourseGameplayEffect.h"
#include "Game/GameplayAbilitySystem/GASCourseNativeGameplayTags.h"
#include "Game/Systems/Healing/GASCourseHealingExecution.h"
struct GASCourseDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(IncomingDamage);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalDamageMultiplier);
GASCourseDamageStatics()
{
DEFINE_ATTRIBUTE_CAPTUREDEF(UGASCourseHealthAttributeSet, IncomingDamage, Source, true);
DEFINE_ATTRIBUTE_CAPTUREDEF(UGASCourseHealthAttributeSet, CriticalChance, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UGASCourseHealthAttributeSet, CriticalDamageMultiplier, Source, false);
}
};
static const GASCourseDamageStatics& DamageStatics()
{
static GASCourseDamageStatics DStatics;
return DStatics;
}
UGASCourseDamageExecution::UGASCourseDamageExecution()
{
RelevantAttributesToCapture.Add(DamageStatics().IncomingDamageDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalChanceDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalDamageMultiplierDef);
}
void UGASCourseDamageExecution::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
UGASCourseAbilitySystemComponent* TargetAbilitySystemComponent = Cast<UGASCourseAbilitySystemComponent>(ExecutionParams.GetTargetAbilitySystemComponent());
UGASCourseAbilitySystemComponent* SourceAbilitySystemComponent = Cast<UGASCourseAbilitySystemComponent>(ExecutionParams.GetSourceAbilitySystemComponent());
AActor* SourceActor = SourceAbilitySystemComponent ? SourceAbilitySystemComponent->GetAvatarActor() : nullptr;
AActor* TargetActor = TargetAbilitySystemComponent ? TargetAbilitySystemComponent->GetAvatarActor() : nullptr;
FGameplayEffectSpec* Spec = ExecutionParams.GetOwningSpecForPreExecuteMod();
// Gather the tags from the source and target as that can affect which buffs should be used
const FGameplayTagContainer* SourceTags = Spec->CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec->CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
bool bUsingCachedDamage = false;
bool bCriticalHit = false;
float Damage = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().IncomingDamageDef, EvaluationParameters, Damage);
// Add SetByCaller damage if it exists
Damage += FMath::Max<float>(Spec->GetSetByCallerMagnitude(Data_IncomingDamage, false, -1.0f), 0.0f);
if (Spec->DynamicGrantedTags.HasTagExact(Data_DamageOverTime))
{
UE_LOG(LogTemp, Warning, TEXT("Damage Over Time"));
float CachedDamage = Spec->GetSetByCallerMagnitude(Data_CachedDamage);
if (CachedDamage > 0.0f)
{
Damage = CachedDamage;
UE_LOG(LogTemp, Warning, TEXT("Using Cached Damage"));
// Set the Target's damage meta attribute
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().IncomingDamageProperty, EGameplayModOp::Additive, Damage));
bUsingCachedDamage = true;
}
}
if (bUsingCachedDamage == false)
{
/*
* Critical Chance + Critical Damage
*/
float CriticalChance = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalChanceDef, EvaluationParameters, CriticalChance);
float CriticalDamageMultiplier = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalDamageMultiplierDef, EvaluationParameters, CriticalDamageMultiplier);
float RolledChancePercentage = FMath::RandRange(0.0f, 1.0f);
bCriticalHit = RolledChancePercentage <= CriticalChance;
if (bCriticalHit)
{
Damage += FMath::Floor(Damage * CriticalDamageMultiplier);
}
if (Damage > 0.f)
{
// Set the Target's damage meta attribute
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().IncomingDamageProperty, EGameplayModOp::Additive, Damage));
}
//Store damage as cached damage
Spec->SetSetByCallerMagnitude(Data_CachedDamage, Damage);
}
UGASCourseGameplayEffect* IncomingHealingGameplayEffect = NewObject<UGASCourseGameplayEffect>(GetTransientPackage());
IncomingHealingGameplayEffect->DurationPolicy = EGameplayEffectDurationType::Instant;
if(UGASCourseGameplayEffect* HealingEffect = Cast<UGASCourseGameplayEffect>(IncomingHealingGameplayEffect))
{
FGameplayEffectExecutionDefinition HealingExecutionDefinition;
HealingExecutionDefinition.CalculationClass = LoadClass<UGASCourseHealingExecution>(SourceActor, TEXT("/Game/GASCourse/Game/Systems/Healing/HealingExecution_Base.HealingExecution_Base_C"));
if(HealingExecutionDefinition.CalculationClass)
{
HealingEffect->Executions.Emplace(HealingExecutionDefinition);
FGameplayEffectContext* ContextHandle = UAbilitySystemGlobals::Get().AllocGameplayEffectContext();
ContextHandle->AddInstigator(SourceActor, SourceActor);
const FGameplayEffectSpecHandle HealingEffectHandle =FGameplayEffectSpecHandle(new FGameplayEffectSpec(HealingEffect, FGameplayEffectContextHandle(ContextHandle), 1.0f));
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(HealingEffectHandle, Data_IncomingHealing, Damage);
UAbilitySystemBlueprintLibrary::AddGrantedTags(HealingEffectHandle, Spec->DynamicGrantedTags);
SourceAbilitySystemComponent->ApplyGameplayEffectSpecToTarget(*HealingEffectHandle.Data.Get(), SourceAbilitySystemComponent);
}
}
// Broadcast damages to Target ASC & SourceASC
if (TargetAbilitySystemComponent && SourceAbilitySystemComponent)
{
FGameplayEventData DamageDealtPayload;
DamageDealtPayload.Instigator = SourceAbilitySystemComponent->GetAvatarActor();
DamageDealtPayload.Target = TargetAbilitySystemComponent->GetAvatarActor();
DamageDealtPayload.EventMagnitude = Damage;
DamageDealtPayload.ContextHandle = Spec->GetContext();
DamageDealtPayload.InstigatorTags = Spec->DynamicGrantedTags;
if(Spec->GetContext().GetHitResult())
{
FHitResult HitResultFromContext = *Spec->GetContext().GetHitResult();
DamageDealtPayload.TargetData = UAbilitySystemBlueprintLibrary::AbilityTargetDataFromHitResult(HitResultFromContext);
}
if (bCriticalHit)
{
DamageDealtPayload.InstigatorTags.AddTag(DamageType_Critical);
}
if(TargetAbilitySystemComponent->HasMatchingGameplayTag(Status_Death))
{
return;
}
SourceAbilitySystemComponent->HandleGameplayEvent(FGameplayTag::RequestGameplayTag(FName("Event.Gameplay.OnDamageDealt")), &DamageDealtPayload);
TargetAbilitySystemComponent->HandleGameplayEvent(FGameplayTag::RequestGameplayTag(FName("Event.Gameplay.OnDamageReceived")), &DamageDealtPayload);
//TODO: Instead of sending event, pass in status effect tag into gameplay status table
TargetAbilitySystemComponent->ApplyGameplayStatusEffect(TargetAbilitySystemComponent, SourceAbilitySystemComponent, Spec->DynamicGrantedTags);
}
}
Here are the tag definitions involved so far inside of the native gameplay tags class:
UE_DEFINE_GAMEPLAY_TAG(Data_CachedDamage, "Data.CachedDamage")
UE_DEFINE_GAMEPLAY_TAG(Data_DamageOverTime, "Data.DamageOverTime")
UE_DEFINE_GAMEPLAY_TAG(DamageType_Critical, "Damage.Type.Critical")
To wrap up the code side of the implementation, let’s look at how we apply damage:
bool UGASCourseASCBlueprintLibrary::ApplyDamageToTarget_Internal(AActor* Target, AActor* Instigator, float Damage, const FDamageContext& DamageContext, UGameplayEffect* GameplayEffect)
{
if(!Instigator || !Target)
{
return false;
}
if(AGASCoursePlayerState* InstigatorPlayerState = Cast<AGASCoursePlayerState>(Instigator))
{
Instigator = InstigatorPlayerState->GetPawn();
}
if(AGASCoursePlayerState* TargetPlayerState = Cast<AGASCoursePlayerState>(Target))
{
Target = TargetPlayerState->GetPawn();
}
//TODO: Add check to verify ability system component + consider damage/health interface for Non-GAS actors
if(AGASCourseCharacter* TargetCharacter = Cast<AGASCourseCharacter>(Target))
{
if(AGASCourseCharacter* InstigatorCharacter = Cast<AGASCourseCharacter>(Instigator))
{
UGASCourseAbilitySystemComponent* TargetASC = TargetCharacter->GetAbilitySystemComponent();
check(TargetASC);
UGASCourseAbilitySystemComponent* InstigatorASC = InstigatorCharacter->GetAbilitySystemComponent();
check(InstigatorASC);
if(UGASCourseGameplayEffect* DamageEffect = Cast<UGASCourseGameplayEffect>(GameplayEffect))
{
FGameplayEffectExecutionDefinition DamageExecutionDefinition;
DamageExecutionDefinition.CalculationClass = LoadClass<UGASCourseDamageExecution>(Instigator, TEXT("/Game/GASCourse/Game/Systems/Damage/DamageExecution_Base.DamageExecution_Base_C"));
DamageEffect->Executions.Emplace(DamageExecutionDefinition);
if (DamageExecutionDefinition.CalculationClass)
{
UE_LOG(LogTemp, Warning, TEXT("Damage Calculation: %s"), *DamageExecutionDefinition.CalculationClass->GetName());
}
UE_LOG(LogTemp, Warning, TEXT("Damage Calculation is not valid!"));
FGameplayEffectContext* ContextHandle = UAbilitySystemGlobals::Get().AllocGameplayEffectContext();
ContextHandle->AddInstigator(Instigator, Instigator);
const FGameplayEffectSpecHandle DamageEffectHandle = FGameplayEffectSpecHandle(new FGameplayEffectSpec(DamageEffect, FGameplayEffectContextHandle(ContextHandle), 1.0f));
AssignTagSetByCallerMagnitude(DamageEffectHandle, Data_IncomingDamage, Damage);
//Pass in cache value through tag.
if (DamageEffect->DurationPolicy == EGameplayEffectDurationType::HasDuration && DamageEffect->Period.GetValue() > 0.0f)
{
AddGrantedTags(DamageEffectHandle, FGameplayTagContainer(Data_DamageOverTime));
}
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;
}
It’s here that we pass in the Data_DamageOverTime
tag label into the damage pipeline so that the execution class can properly filter for it in order to determine if critical chance should be rolled, or if we should use cached damage.
//Pass in cache value through tag.
if (DamageEffect->DurationPolicy == EGameplayEffectDurationType::HasDuration && DamageEffect->Period.GetValue() > 0.0f)
{
AddGrantedTags(DamageEffectHandle, FGameplayTagContainer(Data_DamageOverTime));
}
Now that we are properly calculating critical chance and critical damage, and passing along whether the hit was critical via gameplay tag in the Instigator Tags layer of the payload, we can use this to update the damage number UI:
For our damage number, we update the text to read the word CRITICAL!, update the text to be red, and play a unique animation to emphasize that the hit was critical.
Here is the final result:
Let’s not forget that we also need to ensure we are initializing both the Critical Chance and Critical Damage Multiplier attributes for our player character:
By using attributes, we create an extra layer of gameplay modification of these attributes that players can actively spec into, or have other gameplay mechanics modify these values at runtime to make gameplay more dynamic!
There are still things for my to verify, such as whether or not the damage caching will work when two DoT effects are applied and have differing outcomes when it comes to critical damage calculation with critical chance rolls. If something needs to change as a result of this issue, I will update this blog post with the relevant information.
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:
Ability Cost Gameplay Loop