Note: If you're new to execution calculations, I'd recommend starting with my previous post which covers them in detail, then coming back here.
What is attribute capturing and why would you use it
Attribute capturing refers to directly exposing certain attributes to your execution calculation from attribute sets, to be used in execution calculations.
Capturing attributes lets you handle things like damage in a more complex way. So for example, you could have advantages and weaknesses against certain damage types, or an attribute like armor that should reduce incoming damage. This is just two use cases, but once you learn how to do it, you should naturally be able to see in what other ways they can be used. In this example, I will show how to get attribute magnitude from captured attributes and using them in calculations, which is the most common way of using captured attributes.
Capturing attributes
Defining the struct, declaring and defining attribute capture definitions
In this post, I will show how to use a static struct to access all your captured attributes, as a good performance-aware solution. You are also free to choose not to use the struct.
For the struct approach, the first step is to go to the.cpp of the execution calculation, and define your struct there. In the struct, we first use the DECLARE_ATTRIBUTE_CAPTUREDEF() macro, passing in the name of your attribute. After that, we make the constructor of the struct, and in the constructor we use the DEFINE_ATTRIBUTE_CAPTUREDEF() macro, passing in the attribute set that has the attribute, the attribute from it, whether the attribute from the Target or Source should be used (the Target is the ASC the ExecCalc is outputting its result to, and the Source is the ASC that called it), and a bool for if the captured attribute should be snapshotted (if the attribute should be frozen at ExecCalc GE application (snapshotted) or if the value should be read at execution time). In my case, I will show 2 examples of non-snapshotted captured attributes, Armor and ArmorPenetration, one from the Target and one from the Source.
struct ComplyDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration);
ComplyDamageStatics()
{
DEFINE_ATTRIBUTE_CAPTUREDEF(UComplyAttributeSet, Armor, Target, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UComplyAttributeSet, ArmorPenetration, Source, false);
}
};
Next, also in the cpp file, you would define a static const function that simply returns the struct as static. The reason why we do all this is so that every time this function is called, the same object is always returned instead of always reconstructing (due to it being static) which helps with performance when you have a lot of captured attributes, and the object should be identical everywhere anyways.
static const ComplyDamageStatics& DamageStatics()
{
static ComplyDamageStatics DStatics;
return DStatics;
}
Registering attributes to capture
The last step for capturing attributes is in the constructor of the ExecCalc, where we access the RelevantAttributesToCapture array, calling Add and passing in the FGameplayEffectAttributeCaptureDefinition of our attribute from the struct, which is just the same attribute but with Def at the end.
UExecCalc_Damage::UExecCalc_Damage()
{
RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
RelevantAttributesToCapture.Add(DamageStatics().ArmorPenetrationDef);
}
Accessing the captured attributes
After getting your owning spec by calling GetOwningSpec() on the ExecutionParams, we can create variables of type FGameplayTagContainer* where we will store the CapturedTarget/SourceTags, by calling GetAggregatedTags() on the container.
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
Next, we create a local FAggregatorEvaluateParameters struct, then set the Target/SourceTags from the struct to be the variables we just created. We will use this struct to access the captured attributes when doing things like getting the captured attribute's magnitude.
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.TargetTags = TargetTags;
EvaluationParameters.SourceTags = SourceTags;
Getting captured attribute magnitude
Once all this is set up, getting the attribute magnitude is very simple.
We first create a local float variable that will store the magnitude, then take the ExecutionParams and call AttemptCalculateCapturedAttributeMagnitude() which takes in our captured attribute definition (which we get from our struct and the attribute with Def at the end), the FAggregatorEvaluateParameters we created, and the local variable we created that will get the magnitude passed into it. After that, it's also a good idea to either use FMath::Max on the variable to ensure you never get a value below 0, or clamping using FMath::Clamp to ensure your formulas will never break, unless you account for those manually. Armor should never go below 0, so I ensure that using FMath::Max, and I clamp armor penetration between 0 and 1.0 since every 0.1 armor penetration reduces effective armor by 10%, so values above 1.0 would result in negative armor after the calculation which is not intended.
float Armor = 0.f; ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
DamageStatics().ArmorDef, EvaluationParameters, Armor
);
Armor = FMath::Max<float>(0.f, Armor);
float ArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
DamageStatics().ArmorPenetrationDef, EvaluationParameters, ArmorPenetration
);
ArmorPenetration = FMath::Clamp(ArmorPenetration, 0.f, 1.f);
Using captured attribute magnitude in calculations
Now using the magnitudes is as simple as it would be to use any other float.
In my case I have an armor constant that decides how fast armor damage reduction scales and a cap for how much damage the armor can reduce, and the effective armor is reduced by 10% per 0.1 armor penetration (max 1). I then calculate the damage reduction, then the final damage which is the original damage multiplied by a multiplier, reduced by the damage reduction, then passing in the final damage as the calculation result into my IncomingDamage meta attribute.
// Armor constant decides how fast armor damage reduction scales
// Armor cap decides the highest possible damage reduction
const float ArmorConstant = DamageConfig->ArmorConstant;
const float ArmorCap = DamageConfig->ArmorReductionCap;
// Effective armor is the original armor, reduced by 10% per 0.1 armor penetration
const float EffectiveArmor = Armor * (1.f - ArmorPenetration);
const float DamageReduction = FMath::Min(
EffectiveArmor / (EffectiveArmor + ArmorConstant), ArmorCap
);
const float FinalDamage = (Damage * Multiplier) * (1.f - DamageReduction);
const FGameplayModifierEvaluatedData EvaluatedData(
UComplyAttributeSet::GetIncomingDamageAttribute(),
EGameplayModOp::Additive, FinalDamage
);
OutExecutionOutput.AddOutputModifier(EvaluatedData);
This is just my own way I used captured attributes, but you can go as simple or as complicated as you want or need.
Conclusion
Attribute capturing in execution calculations is a way to make the already powerful execution calculations even better. Capturing armor and armor penetration in my case allows me to make combat feel more intuitive and varied across different enemies, and is particularly good for upgrades and difficulty scaling instead of simply bumping up health continuously.
If you have any questions or feedback, feel free to contact me on LinkedIn, or email me: petric.marko04@gmail.com
Feel free to also check out my website where I have everything in one place, plus additional content!
Feel free to also check out my website where I have everything in one place, plus additional content!
Top comments (0)