Skip to content

Instantly share code, notes, and snippets.

@mklabs
Created March 13, 2022 11:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mklabs/fcf8b3c355fdc43b3738957687b492e7 to your computer and use it in GitHub Desktop.
Save mklabs/fcf8b3c355fdc43b3738957687b492e7 to your computer and use it in GitHub Desktop.
Example of Gameplay Effect Execution Calculation with logic to handle health and stamina damage if blocking, and dealing with parry frames which nullifies damage and sends an event to trigger a "parried" ability on source character.
// Copyright 2020 Mickael Daniel.
#include "Abilities/RPGDamageExecution.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "RPGBlueprintLibrary.h"
#include "Abilities/RPGAttributeSet.h"
#include "Characters/RPGCharacterBase.h"
#include "Items/RPGWeaponActor.h"
struct FRPGDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);
DECLARE_ATTRIBUTE_CAPTUREDEF(StaminaDamage);
FRPGDamageStatics()
{
// Capture the Target's DefensePower attribute. Do not snapshot it, because we want to use the health value at the moment we apply the execution.
// DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false);
// Capture the Source's AttackPower. We do want to snapshot this at the moment we create the GameplayEffectSpec that will execute the damage.
// (imagine we fire a projectile: we create the GE Spec when the projectile is fired. When it hits the target, we want to use the AttackPower at the moment
// the projectile was launched, not when it hits).
// DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, true);
// Also capture the source's raw Damage, which is normally passed in directly via the execution
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, StaminaDamage, Source, true);
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, Damage, Source, true);
}
};
static const FRPGDamageStatics& DamageStatics()
{
static FRPGDamageStatics DamageStatics;
return DamageStatics;
}
URPGDamageExecution::URPGDamageExecution()
{
VulnerableAbilityTag = FGameplayTag::RequestGameplayTag(FName("Event.Ability.Vulnerable"));
RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
RelevantAttributesToCapture.Add(DamageStatics().StaminaDamageDef);
}
void URPGDamageExecution::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
UAbilitySystemComponent* TargetAbilitySystemComponent = ExecutionParams.GetTargetAbilitySystemComponent();
UAbilitySystemComponent* SourceAbilitySystemComponent = ExecutionParams.GetSourceAbilitySystemComponent();
AActor* SourceActor = SourceAbilitySystemComponent ? SourceAbilitySystemComponent->GetAvatarActor() : nullptr;
AActor* TargetActor = TargetAbilitySystemComponent ? TargetAbilitySystemComponent->GetAvatarActor() : nullptr;
ARPGCharacterBase* SourceCharacter = Cast<ARPGCharacterBase>(SourceActor);
ARPGCharacterBase* TargetCharacter = Cast<ARPGCharacterBase>(TargetActor);
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
// 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;
float AttackPower = 1.0f;
float Stability = 0.0f;
float GuardAbsorption = 0.0f;
// TODO: Better way to figure out from which weapon to get Attack Power and Stability,
// For now right left weapon if equipped takes precedence over right weapon for Defense
// when guarding (Stability, GuardAbsorption)
//
// TODO: Handle types of Attack (Physical, Magic, Fire, ...)
if (SourceCharacter && SourceCharacter->GetRightEquippedWeapon())
{
AttackPower = SourceCharacter->GetRightEquippedWeapon()->WeaponAttributes->AttackPowerPhysical;
}
if (TargetCharacter)
{
if (TargetCharacter->GetRightEquippedWeapon())
{
Stability = TargetCharacter->GetRightEquippedWeapon()->WeaponAttributes->Stability;
GuardAbsorption = TargetCharacter->GetRightEquippedWeapon()->WeaponAttributes->GuardAbsorptionPhysical;
}
if (TargetCharacter->GetLeftEquippedWeapon())
{
Stability = TargetCharacter->GetLeftEquippedWeapon()->WeaponAttributes->Stability;
GuardAbsorption = TargetCharacter->GetLeftEquippedWeapon()->WeaponAttributes->GuardAbsorptionPhysical;
}
}
// --------------------------------------
// Damage Done = (Damage * AttackPower ) * (1 - (GuardAbsorption / 100)
// If DefensePower is 0, it is treated as 1.0
// --------------------------------------
float Damage = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);
float DamageDone = Damage * AttackPower;
// --------------------------------------
// Stamina Damage Done = (StaminaDamage * AttackPower) * (1 - (Stability / 100))
// If Stability is 0, Stamina Damage is now reduced and takes full effect
// --------------------------------------
float StaminaDamage = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().StaminaDamageDef, EvaluationParameters, StaminaDamage);
float StaminaDamageDone = 0.0f;
// Only apply StaminaDamage and reduce Damage by GuardAbsorption if the target is blocking
if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag("State.Blocking")))
{
// And if the Source Actor is attacking from the front
const float Angle = URPGBlueprintLibrary::FindDegreeToTarget(TargetActor, SourceActor);
if (FMath::Abs(Angle) < 90.0f)
{
StaminaDamageDone = (StaminaDamage * AttackPower) * (1 - (Stability / 100));
DamageDone = DamageDone * (1 - (GuardAbsorption / 100));
}
}
// Handle Parry Frames if active on target
if (TargetTags->HasTag(FGameplayTag::RequestGameplayTag("State.Parry")))
{
if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag("State.Parry.Partial")))
{
DamageDone = DamageDone * 0.25f;
}
if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag("State.Parry.Perfect")))
{
DamageDone = 0.0f;
}
// Trigger back vulnerable ability to Source Character via Event
FGameplayEventData Payload;
SourceCharacter->ReplicatedSendGameplayEvent(VulnerableAbilityTag, Payload);
}
if (DamageDone > 0.0f)
{
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().DamageProperty, EGameplayModOp::Additive, DamageDone));
}
if (StaminaDamageDone > 0.0f)
{
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().StaminaDamageProperty, EGameplayModOp::Additive, StaminaDamageDone));
}
}
// Copyright 2020 Mickael Daniel.
#pragma once
#include "CoreMinimal.h"
#include "GameplayEffectExecutionCalculation.h"
#include "RPGGameplayAbility.h"
#include "RPGDamageExecution.generated.h"
/**
* A damage execution, which allows doing damage by combining a raw Damage Number with AttackPower and DefensePower.
*
* It also checks target state via tags to modify the applied damage, which can be done to Health or Stamina.
*/
UCLASS()
class ARPG_API URPGDamageExecution : public UGameplayEffectExecutionCalculation
{
GENERATED_BODY()
public:
URPGDamageExecution();
virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
private:
/** Cached Gameplay Tags */
FGameplayTag VulnerableAbilityTag;
};
@mklabs
Copy link
Author

mklabs commented Mar 13, 2022

I would probably do that a bit differently now, but that's the gist of it.

@Valera94
Copy link

Hello. How would you change the execution?

@mklabs
Copy link
Author

mklabs commented Feb 23, 2024

@Valera94 At first glance, I would say:

  • Use of native gameplay tags instead of FGameplayTag::RequestGameplayTag
  • Split each part of the executions in its own method (Equipment part, Blocking part, Parry part)
  • DamageStatics should be named FDamageStatics
  • Rework DamageStatics part and avoid multiple invocation / allocation. It can be done only once.

This code is pretty old, I think it was implemented in 4.26 or 4.27.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment