1. Execution Calculations基础概念
在UE5的Gameplay Ability System(GAS)中,Execution Calculations(执行计算)是一个强大的工具,它允许开发者在Gameplay Effect执行期间进行复杂的自定义计算。想象一下你正在设计一个RPG游戏的战斗系统,每次攻击造成的伤害不仅取决于攻击力,还要考虑目标的防御、暴击概率、护甲穿透等多项因素 - 这正是Execution Calculations大显身手的地方。
与ModifierMagnitudeCalculation(MMC)不同,Execution Calculations可以同时修改多个属性。比如在一次攻击中,你不仅可以计算伤害值,还能同时触发暴击效果、减少目标护甲值、施加debuff状态等。这种灵活性让复杂的战斗公式变得容易实现。
我在实际项目中发现,Execution Calculations特别适合处理以下场景:
- 需要同时考虑攻击方和防御方多个属性的伤害计算
- 带有随机判定的战斗机制(如暴击、格挡)
- 需要动态调整计算系数的成长系统
- 涉及多个属性联动的特殊效果
2. 创建基础伤害计算类
2.1 类结构与属性捕获
让我们从创建一个基础的伤害计算类开始。首先在C++中新建一个继承自UGameplayEffectExecutionCalculation的类:
// ExecCalc_Damage.h #pragma once #include "CoreMinimal.h" #include "GameplayEffectExecutionCalculation.h" #include "ExecCalc_Damage.generated.h" UCLASS() class YOURPROJECT_API UExecCalc_Damage : public UGameplayEffectExecutionCalculation { GENERATED_BODY() public: UExecCalc_Damage(); virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override; };属性捕获是Execution Calculations的核心机制。我们可以定义一个内部结构体来管理需要捕获的属性:
// 内部结构体,不需要外部访问 struct FDamageStatics { DECLARE_ATTRIBUTE_CAPTUREDEF(Armor); // 护甲属性 DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower); // 攻击力 FDamageStatics() { // 捕获目标护甲属性 DEFINE_ATTRIBUTE_CAPTUREDEF(UYourAttributeSet, Armor, Target, false); // 捕获攻击方攻击力属性 DEFINE_ATTRIBUTE_CAPTUREDEF(UYourAttributeSet, AttackPower, Source, false); } }; static const FDamageStatics& DamageStatics() { static FDamageStatics DStatics; return DStatics; }2.2 执行函数实现
Execute_Implementation是实际计算发生的地方。下面是一个基础实现框架:
void UExecCalc_Damage::Execute_Implementation( const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const { // 获取源和目标ASC const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent(); const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent(); // 获取角色实例 AActor* SourceActor = SourceASC ? SourceASC->GetAvatarActor() : nullptr; AActor* TargetActor = TargetASC ? TargetASC->GetAvatarActor() : nullptr; // 获取GameplayEffectSpec const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec(); // 设置评估参数 FAggregatorEvaluateParameters EvaluationParameters; EvaluationParameters.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags(); EvaluationParameters.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags(); // 获取基础伤害值(通过SetByCaller传入) float Damage = Spec.GetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage"))); // 获取护甲和攻击力属性 float TargetArmor = 0.f; ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, TargetArmor); TargetArmor = FMath::Max(0.f, TargetArmor); float SourceAttackPower = 0.f; ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().AttackPowerDef, EvaluationParameters, SourceAttackPower); SourceAttackPower = FMath::Max(0.f, SourceAttackPower); // 基础伤害计算 Damage += SourceAttackPower * 0.5f; // 攻击力贡献 Damage *= (100.f - TargetArmor * 0.3f) / 100.f; // 护甲减伤 // 输出计算结果 const FGameplayModifierEvaluatedData EvaluatedData(UYourAttributeSet::GetIncomingDamageAttribute(), EGameplayModOp::Additive, Damage); OutExecutionOutput.AddOutputModifier(EvaluatedData); }3. 实现暴击与格挡机制
3.1 暴击系统实现
暴击是RPG游戏的核心机制之一。我们需要在结构体中添加暴击相关属性:
struct FDamageStatics { // ...已有属性... DECLARE_ATTRIBUTE_CAPTUREDEF(CritChance); // 暴击率 DECLARE_ATTRIBUTE_CAPTUREDEF(CritDamage); // 暴击伤害 DECLARE_ATTRIBUTE_CAPTUREDEF(CritResist); // 暴击抵抗 FDamageStatics() { // ...已有捕获... DEFINE_ATTRIBUTE_CAPTUREDEF(UYourAttributeSet, CritChance, Source, false); DEFINE_ATTRIBUTE_CAPTUREDEF(UYourAttributeSet, CritDamage, Source, false); DEFINE_ATTRIBUTE_CAPTUREDEF(UYourAttributeSet, CritResist, Target, false); } };然后在Execute_Implementation中添加暴击计算逻辑:
// 获取暴击相关属性 float CritChance = 0.f, CritDamage = 0.f, CritResist = 0.f; ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CritChanceDef, EvaluationParameters, CritChance); ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CritDamageDef, EvaluationParameters, CritDamage); ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CritResistDef, EvaluationParameters, CritResist); // 计算实际暴击率 float EffectiveCritChance = FMath::Clamp(CritChance - CritResist * 0.5f, 0.f, 100.f); bool bCriticalHit = FMath::RandRange(1, 100) <= EffectiveCritChance; // 应用暴击伤害 if(bCriticalHit) { float CritMultiplier = 2.0f + CritDamage / 100.f; // 基础2倍+额外暴击伤害 Damage *= CritMultiplier; // 可以在这里触发暴击特效或音效 // ... }3.2 格挡机制实现
格挡是另一个常见的防御机制。添加格挡相关属性:
struct FDamageStatics { // ...已有属性... DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance); // 格挡率 DECLARE_ATTRIBUTE_CAPTUREDEF(BlockEffectiveness); // 格挡效果 FDamageStatics() { // ...已有捕获... DEFINE_ATTRIBUTE_CAPTUREDEF(UYourAttributeSet, BlockChance, Target, false); DEFINE_ATTRIBUTE_CAPTUREDEF(UYourAttributeSet, BlockEffectiveness, Target, false); } };格挡计算逻辑:
// 获取格挡属性 float BlockChance = 0.f, BlockEffectiveness = 0.f; ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters, BlockChance); ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockEffectivenessDef, EvaluationParameters, BlockEffectiveness); // 判断是否触发格挡 bool bBlocked = FMath::RandRange(1, 100) <= BlockChance; if(bBlocked) { // 格挡效果通常在50%-100%之间 float BlockPercent = 0.5f + BlockEffectiveness * 0.005f; Damage *= (1.f - FMath::Clamp(BlockPercent, 0.f, 1.f)); // 触发格挡特效或音效 // ... }4. 高级伤害公式与动态系数
4.1 护甲穿透系统
护甲穿透让攻击者能够部分忽略目标的护甲值。添加相关属性:
struct FDamageStatics { // ...已有属性... DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration); // 护甲穿透 FDamageStatics() { // ...已有捕获... DEFINE_ATTRIBUTE_CAPTUREDEF(UYourAttributeSet, ArmorPenetration, Source, false); } };护甲穿透计算逻辑:
// 获取护甲穿透 float ArmorPenetration = 0.f; ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef, EvaluationParameters, ArmorPenetration); ArmorPenetration = FMath::Max(0.f, ArmorPenetration); // 计算实际护甲值 float EffectiveArmor = TargetArmor * (100.f - ArmorPenetration) / 100.f; EffectiveArmor = FMath::Max(0.f, EffectiveArmor); // 使用EffectiveArmor代替原始护甲值计算减伤 Damage *= (100.f - EffectiveArmor * 0.3f) / 100.f;4.2 使用曲线表动态调整系数
为了让游戏数值随等级平衡,我们可以使用曲线表动态调整各种系数:
// 获取角色等级 IYourLevelInterface* SourceLevelInterface = Cast<IYourLevelInterface>(SourceActor); IYourLevelInterface* TargetLevelInterface = Cast<IYourLevelInterface>(TargetActor); int32 SourceLevel = SourceLevelInterface ? SourceLevelInterface->GetCharacterLevel() : 1; int32 TargetLevel = TargetLevelInterface ? TargetLevelInterface->GetCharacterLevel() : 1; // 从曲线表获取动态系数 UDataTable* CoefficientTable = // 获取你的系数表; static const FString ContextString(TEXT("Damage Coefficient Context")); FArmorPenetrationCoefficient* PenetrationCoeff = CoefficientTable->FindRow<FArmorPenetrationCoefficient>(FName(*FString::FromInt(SourceLevel)), ContextString); FArmorCoefficient* ArmorCoeff = CoefficientTable->FindRow<FArmorCoefficient>(FName(*FString::FromInt(TargetLevel)), ContextString); // 应用动态系数 float EffectiveArmor = TargetArmor * (100.f - ArmorPenetration * PenetrationCoeff->Value) / 100.f; Damage *= (100.f - EffectiveArmor * ArmorCoeff->Value) / 100.f;5. 实战技巧与优化建议
5.1 性能优化
Execution Calculations在服务器上运行,因此性能很重要:
- 最小化属性捕获:只捕获真正需要的属性,减少内存访问
- 避免复杂计算:将复杂计算预先计算并存储在曲线表中
- 使用静态函数:像DamageStatics()这样的辅助函数只初始化一次
- 减少动态分配:避免在Execute函数中创建临时对象
5.2 调试技巧
调试伤害计算可能会很棘手,我常用的方法包括:
// 添加调试输出 if(GEngine) { GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, FString::Printf(TEXT("Damage: %.1f, Crit: %s, Block: %s"), Damage, bCriticalHit ? TEXT("Yes") : TEXT("No"), bBlocked ? TEXT("Yes") : TEXT("No"))); } // 使用UE_LOG UE_LOG(LogTemp, Log, TEXT("Final Damage: %.2f (Base: %.2f, ArmorReduction: %.2f%%)"), Damage, BaseDamage, EffectiveArmor * 0.3f);5.3 扩展思路
Execution Calculations非常灵活,你还可以实现:
- 元素抗性系统:根据不同伤害类型应用不同抗性
- 连击加成:基于连续命中次数增加伤害
- 背刺/弱点攻击:基于攻击角度增加伤害
- 环境加成:考虑地形、天气等因素
我在一个项目中曾实现过基于时间段的伤害加成系统 - 夜晚增加暗影伤害,白天增加光系伤害,只需要在计算时获取游戏世界时间即可。