10 ExecutionCalculation 与 ModifierMagnitudeCalculation(高级计算)
目标:掌握 GAS 两个"公式引擎"——
UGameplayEffectExecutionCalculation(完整伤害管线)与UGameplayModMagnitudeCalculation(单修改器公式),属性捕获的准确语义(快照/非快照、自动更新、PreAttributeChange 不参与)、向 ExecCalc 传数据的四种方式、CAR、以及完整 RPG 伤害计算流水线设计。
10.1 两个类的定位
| 类 | 作用域 | 何时用 |
|---|---|---|
UGameplayEffectExecutionCalculation(ExecCalc) |
整份 GE 的一次"执行",可输出多个属性修改 | 完整伤害管线(伤害-护甲-暴击-吸血一条龙)、复杂结算 |
UGameplayModMagnitudeCalculation(MMC) |
单个 Modifier 的数值计算 | 单个数值公式("攻击力×150% 的伤害") |
两者都通过属性捕获(Attribute Capture)读取来源/目标的属性值参与计算。
MMC 与普通 Modifier 的区别
| 特性 | 普通 Modifier | MMC |
|---|---|---|
| 数值来源 | 固定值 / ScalableFloat / CurveTable | 动态计算(运行时) |
| 属性捕获 | 不支持 | 支持捕获 Source/Target 任意属性 |
| 标签读取 | 不支持 | 支持读取 GE Spec 上的标签 |
| SetByCaller | 需单独配置 | 计算中可直接读取 |
| 可预测 | ✓ | ✓ |
| 适用 GE 类型 | 所有 | Instant / Duration / Infinite / Periodic |
| 复杂度 | 低 | 中-高 |
MMC 可预测(与 ExecCalc 的最大区别)
MMC 可以预测,客户端可以先执行效果不必等服务器确认——这对以下场景至关重要:
- Cost GE:按技能键法力立刻扣除(不用等服务器确认);
- Cooldown GE:冷却立刻开始倒计时;
- 移动速度变化:冲刺时速度立刻变化。
ExecCalc 不可预测,只能 Instant/Periodic、只能服务器运行。
10.2 属性捕获(Attribute Capture)
捕获类型
| 捕获类型 | 含义 |
|---|---|
AttributeCaptureSource::Source |
读取效果来源(施法者)的属性 |
AttributeCaptureSource::Target |
读取效果目标的属性 |
快照(Snapshot)语义(完整时间线)
| 快照 | 来源/目标 | 捕获时机 | 自动更新(Duration/Infinite GE) |
|---|---|---|---|
| 是(快照) | Source | GE Spec 创建时(T0) | 否 |
| 是(快照) | Target | GE 应用时(T1) | 否 |
| 否(非快照) | Source | GE 应用时(T1) | 是(T2 自动重算) |
| 否(非快照) | Target | GE 应用时(T1) | 是(T2 自动重算) |
时间线:
T0: GE Spec 创建(MakeOutgoingGameplayEffectSpec)
T1: GE 应用(ApplyGameplayEffectSpecToTarget)
T2: 被捕获的属性变化
T3: Duration/Infinite GE 持续中...
快照 Source(bSnapshot=true, Source):捕获时刻 = T0,T2 不更新
快照 Target(bSnapshot=true, Target):捕获时刻 = T1,T2 不更新
非快照(bSnapshot=false, 任一端):捕获时刻 = T1,T2 时自动更新 ✓
经典坑: - 投射物伤害 GE 若
bSnapshot=false,飞行期间施法者攻击力变化会影响伤害——大多数投射物应设 bSnapshot=true(发射瞬间定死); - 反之 DoT 想实时受属性变化影响则用 false; - 属性捕获的重新计算不会运行PreAttributeChange()——若其中有钳制逻辑,需要在 MMC/ExecCalc 中自行实现。
10.3 MMC:单修改器公式(完整示例)
// PMC_PoisonMana.h
UCLASS()
class MYGAME_API UPMC_PoisonMana : public UGameplayModMagnitudeCalculation
{
GENERATED_BODY()
public:
UPMC_PoisonMana();
virtual float CalculateBaseMagnitude_Implementation(
const FGameplayEffectSpec& Spec) const override;
private:
FGameplayEffectAttributeCaptureDefinition ManaDef;
FGameplayEffectAttributeCaptureDefinition MaxManaDef;
};
// PMC_PoisonMana.cpp
UPMC_PoisonMana::UPMC_PoisonMana()
{
ManaDef.AttributeToCapture = UMyAttributeSet::GetManaAttribute();
ManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
ManaDef.bSnapshot = false; // 非快照,自动更新
RelevantAttributesToCapture.Add(ManaDef); // 必须注册!
MaxManaDef.AttributeToCapture = UMyAttributeSet::GetMaxManaAttribute();
MaxManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
MaxManaDef.bSnapshot = false;
RelevantAttributesToCapture.Add(MaxManaDef);
}
float UPMC_PoisonMana::CalculateBaseMagnitude_Implementation(
const FGameplayEffectSpec& Spec) const
{
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvalParams;
EvalParams.SourceTags = SourceTags;
EvalParams.TargetTags = TargetTags;
float Mana = 0.f, MaxMana = 1.f;
GetCapturedAttributeMagnitude(ManaDef, Spec, EvalParams, Mana);
GetCapturedAttributeMagnitude(MaxManaDef, Spec, EvalParams, MaxMana);
Mana = FMath::Max(Mana, 0.f);
MaxMana = FMath::Max(MaxMana, 1.f); // 避免除零
float PoisonDamage = -20.f; // 基础毒伤害
if (Mana / MaxMana > 0.5f) PoisonDamage *= 2.f; // 法力过半双倍
if (TargetTags->HasTagExact(
FGameplayTag::RequestGameplayTag(FName("Status.WeakToPoisonMana"))))
PoisonDamage *= 2.f; // 弱点标签再翻倍
return PoisonDamage;
}
GE 配置:Modifier 数值来源选 Custom Calculation Class,指向该 MMC,绑定目标属性(如 Damage Meta)。
常见陷阱:忘记把捕获定义加入
RelevantAttributesToCapture会报错/拿不到值。
10.4 ExecutionCalculation:完整伤害管线
属性捕获设置模式(唯一命名 struct)
// 重要:每个 ExecCalc 的 struct 必须有唯一名称!
// 使用相同名称会导致属性捕获行为异常(捕获到错误的属性值)
struct FDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
DECLARE_ATTRIBUTE_CAPTUREDEF(Shield);
DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);
DECLARE_ATTRIBUTE_CAPTUREDEF(CritChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CritDamage);
FDamageStatics()
{
DEFINE_ATTRIBUTE_CAPTUREDEF(UMyAttributeSet, AttackPower, Source, true); // 快照
DEFINE_ATTRIBUTE_CAPTUREDEF(UMyAttributeSet, CritChance, Source, true);
DEFINE_ATTRIBUTE_CAPTUREDEF(UMyAttributeSet, CritDamage, Source, true);
DEFINE_ATTRIBUTE_CAPTUREDEF(UMyAttributeSet, Armor, Target, false); // 非快照
DEFINE_ATTRIBUTE_CAPTUREDEF(UMyAttributeSet, Shield, Target, false);
}
};
static const FDamageStatics& DmgStatics()
{
static FDamageStatics Statics;
return Statics;
}
UGD_DamageExecCalc::UGD_DamageExecCalc()
{
RelevantAttributesToCapture.Add(DmgStatics().AttackPowerDef);
RelevantAttributesToCapture.Add(DmgStatics().CritChanceDef);
RelevantAttributesToCapture.Add(DmgStatics().CritDamageDef);
RelevantAttributesToCapture.Add(DmgStatics().ArmorDef);
RelevantAttributesToCapture.Add(DmgStatics().ShieldDef);
}
完整执行逻辑(伤害+暴击+护甲+护盾)
void UGD_DamageExecCalc::Execute_Implementation(
const FGameplayEffectCustomExecutionParameters& ExecutionParams,
FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
if (!SourceASC || !TargetASC) return;
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvalParams;
EvalParams.SourceTags = SourceTags;
EvalParams.TargetTags = TargetTags;
// 1. 读取 SetByCaller 基础伤害
float BaseDamage = FMath::Max(
Spec.GetSetByCallerMagnitude(
FGameplayTag::RequestGameplayTag(FName("Data.Damage")), false, -1.f), 0.f);
// 2. 捕获来源攻击力加成
float AttackPower = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
DmgStatics().AttackPowerDef, EvalParams, AttackPower);
AttackPower = FMath::Max(AttackPower, 0.f);
float TotalDamage = BaseDamage + AttackPower;
// 3. 暴击
float CritChance = 0.f, CritDamage = 150.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
DmgStatics().CritChanceDef, EvalParams, CritChance);
CritChance = FMath::Clamp(CritChance, 0.f, 100.f);
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
DmgStatics().CritDamageDef, EvalParams, CritDamage);
if (FMath::RandRange(0.f, 100.f) < CritChance)
TotalDamage *= (CritDamage / 100.f); // 暴击信息可写入自定义 EffectContext 供 Cue 用
// 4. 护甲减伤:Damage × (100 / (100 + Armor))
float Armor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
DmgStatics().ArmorDef, EvalParams, Armor);
Armor = FMath::Max(Armor, 0.f);
float MitigatedDamage = TotalDamage * (100.f / (100.f + Armor));
// 5. 护盾吸收
float Shield = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
DmgStatics().ShieldDef, EvalParams, Shield);
Shield = FMath::Max(Shield, 0.f);
float DamageToShield = FMath::Min(MitigatedDamage, Shield);
float DamageToHealth = MitigatedDamage - DamageToShield;
// 6. 输出结果(可同时修改多个属性)
if (DamageToHealth > 0.f)
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(
DmgStatics().DamageProperty, EGameplayModOp::Additive, DamageToHealth));
if (DamageToShield > 0.f)
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(
DmgStatics().ShieldProperty, EGameplayModOp::Additive, -DamageToShield));
}
然后 AttributeSet 的 PostGameplayEffectExecute 把 Damage 转成真实扣血(第 04 章已实现)。
ExecCalc 的限制
| 特性 | 说明 |
|---|---|
| 可预测 | ✗ 不可预测 |
| 适用 GE 类型 | 仅 Instant 和 Periodic |
| 实现语言 | 必须 C++ |
| 可修改属性数量 | 任意多个 |
| 运行位置 | 仅服务器 |
10.5 向 ExecCalc 传递数据的四种方式
| 方式 | 复杂度 | 灵活性 | 编辑器可配置 | 推荐场景 |
|---|---|---|---|---|
| SetByCaller | 低 | 高 | ✓ | 运行时动态值(蓄力伤害、技能等级伤害) |
| Backing Data Attribute Calculation Modifier | 中 | 中 | ✓ | 固定配置值(策划硬编码到元属性) |
| Temporary Variable Calculation Modifier | 中 | 中 | ✓ | 独立于属性的临时配置值 |
| GameplayEffectContext | 高 | 最高 | ✗ | 复杂自定义数据(命中部位、武器类型) |
// 方式 1:SetByCaller(最常用)
float Damage = FMath::Max(
Spec.GetSetByCallerMagnitude(
FGameplayTag::RequestGameplayTag(FName("Data.Damage")), false, -1.f), 0.f);
// 方式 2:Backing Attribute(GE 的 CalculationModifier 把值写进元属性)
// GE → Execution Calculation → Calculation Modifiers:
// [0] Modifier Attribute: Damage (Source), Op: Additive, Magnitude: ScalableFloat 50
float Damage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DmgStatics().DamageDef, EvalParams, Damage);
// 方式 3:Temporary Variable(构造函数注册)
UGD_MyExecCalc::UGD_MyExecCalc()
{
ValidTransientAggregatorIdentifiers.AddTag(FGameplayTag::RequestGameplayTag("Data.Damage"));
}
float Damage = 0.f;
ExecutionParams.AttemptCalculateTransientAggregatorMagnitude(
FGameplayTag::RequestGameplayTag("Data.Damage"), EvalParams, Damage);
// 方式 4:自定义 EffectContext(最灵活,可传结构数据)
FMyGameplayEffectContext* MyContext =
static_cast<FMyGameplayEffectContext*>(Spec.GetContext().Get());
if (MyContext) { bool bHeadshot = MyContext->bIsHeadshot; /* ... */ }
10.6 Custom Application Requirement(CAR)
比标签更灵活的 GE 应用条件(详见第 05 章 5.12)。要点回顾:
UCLASS()
class UMyCAR_RequireMinMana : public UGameplayEffectCustomApplicationRequirement
{
GENERATED_BODY()
public:
virtual bool CanApplyGameplayEffect_Implementation(
const UGameplayEffect* GameplayEffect,
const FGameplayEffectSpec& Spec,
UAbilitySystemComponent* ASC) const override
{
return ASC->GetNumericAttribute(UMyAttributeSet::GetManaAttribute()) >= 30.f;
}
};
使用场景:检查属性、检查堆叠上限、修改持续时间(已有实例刷新而非新增)、组合多个条件。
10.7 完整 RPG 伤害流水线设计
多伤害类型(用 Tag 区分,同一 ExecCalc)
if (Spec.DynamicAssetTags.HasTagExact(DamageTypePhysical))
MitigatedDamage = TotalDamage * (100.f / (100.f + Armor));
else if (Spec.DynamicAssetTags.HasTagExact(DamageTypeMagical))
MitigatedDamage = TotalDamage * (100.f / (100.f + MagicResist));
else // 真实伤害
MitigatedDamage = TotalDamage;
护甲穿透
float ArmorPen = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
DmgStatics().ArmorPenDef, EvalParams, ArmorPen);
ArmorPen = FMath::Clamp(ArmorPen, 0.f, 100.f);
float EffectiveArmor = Armor * (1.f - ArmorPen / 100.f);
float MitigatedDamage = TotalDamage * (100.f / (100.f + EffectiveArmor));
元素克制
float ElementalMultiplier = 1.f;
if (AttackElement == Fire && TargetElement == Ice) ElementalMultiplier = 2.f; // 火克冰
else if (AttackElement == Ice && TargetElement == Fire) ElementalMultiplier = 0.5f;
TotalDamage *= ElementalMultiplier;
为什么"伤害管线"要这样设计
GE_Damage (Instant, Execution=UDamageExecCalc)
├─ 捕获:Source.AttackPower/CritChance, Target.Armor/Shield
├─ 计算:SetByCaller基础伤害 + 攻击加成 + 暴击 + 护甲减伤 + 护盾吸收
└─ 输出:Damage(Meta) += 计算值
→ PostGameplayEffectExecute 统一结算
├─ 护盾吸收 → 真实扣血 → 死亡判定
└─ 触发 GameplayCue(受击表现)
好处:策划改公式只改 Execution;吸血/护盾/减伤集中在 AttributeSet;技能只负责"施加这个 GE"。
10.8 性能注意点
- ExecutionCalculation / MMC 在服务端执行,是每次伤害的 CPU 开销点;
- 高频 AOE 命中大量单位时,捕获属性(尤其非快照)有成本,能快照就快照;
- 避免在 Execute 里做
FGameplayTag::RequestGameplayTag(FName(...))字符串查询——用缓存过的静态FGameplayTag成员; - 属性捕获的重新计算不跑
PreAttributeChange(),钳制需自行实现。
10.9 本章验证
- [ ] 用 MMC 实现"攻击力×1.5-防御×0.5"的单属性公式并验证随属性变化;
- [ ] 用 MMC 实现"按目标法力百分比计算毒伤害"(含标签分支);
- [ ] 用 ExecutionCalculation 实现完整伤害管线(SetByCaller + 暴击 + 护甲 + 护盾)并在 PostGameplayEffectExecute 结算;
- [ ] 对比 bSnapshot=true/false 在"施法后属性变化"下的伤害差异;
- [ ] 用四种方式向 ExecCalc 传数据并验证读取;
- [ ] 写一个 CAR(要求目标有"中毒"标签才能应用"剧毒爆发");
- [ ] 给 ExecCalc 加"命中部位"倍率(头部×2,用自定义 EffectContext)。