GAS 系统深度学习指南UE 5.4 / 5.5

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)。

下一章:11 网络预测(Prediction)