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

05 GameplayEffects 效果系统

目标:彻底搞懂 GE——三种时长策略与精确行为、修改器(Modifier)的四种运算与四种数值来源、聚合公式与 Multiply/Divide 陷阱、堆叠规则、GE 的六大标签容器、授予能力、免疫系统、GESpec/SetByCaller/Context、ExecCalc 与数据传递、CAR、Cost/Cooldown 设计。这是 GAS 中内容最多、最容易踩坑的一章,请务必逐节动手验证。


5.1 GE 是什么

UGameplayEffect(GE)是 GAS 中修改属性的规则定义。它是一个纯数据类(通常用蓝图资产配置),描述"以什么方式、修改哪些属性、持续多久、能否叠加"。

GE 不应该包含任何逻辑——逻辑放到 ExecutionCalculation、MMC 或 AttributeSet 回调里。策划通过配置大量 GE 蓝图资产,就能覆盖几乎所有"数值变化"需求。

药方类比:GE 就像一张药方——它只记录"用什么药、用量多少、持续多久",真正的"服药"由 GA 或 ASC 执行;同一张药方可以给不同目标(Target)使用。

引擎里的执行过程:施加 GE 时,ASC 把 GE 的配置(UGameplayEffect CDO)+ 运行时参数(等级、数值、上下文)组装成一份 FGameplayEffectSpec;对 Duration/Infinite 类型,再生成一个 FActiveGameplayEffect 放进 ASC 的 ActiveGameplayEffects 容器(FFastArraySerializer,负责网络同步)。Instant 类型不进入该容器,直接"执行"。

GE 通过两种方式改变属性:

  • Modifier(修改器):直接修改属性值;
  • Execution(执行计算):通过 GameplayEffectExecutionCalculation 进行复杂计算(见第 10 章)。

5.2 三种时长策略(Duration Policy)

策略 属性影响方式 GameplayTag 生命周期
Instant 永久修改 BaseValue 不应用标签(哪怕一帧) 立即生效后移除
HasDuration 临时修改 CurrentValue 应用标签 到期自动移除
Infinite 临时修改 CurrentValue 应用标签 手动移除

5.2.1 Instant(即时)

立即生效,永久修改 BaseValue,应用后立即从 ASC 移除。

使用场景:造成伤害、治疗、拾取金币/经验、初始化属性值。

// Instant GE 修改的 Meta 属性在 PostGameplayEffectExecute 结算
void UMyAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
    if (Data.EvaluatedData.Attribute == GetDamageAttribute())
    {
        const float LocalDamage = GetDamage();
        SetDamage(0.f);   // 重置元属性
        SetHealth(FMath::Clamp(GetHealth() - LocalDamage, 0.f, GetMaxHealth()));
    }
}

5.2.2 HasDuration(持续)

临时修改 CurrentValue,到期自动移除,属性恢复原值。

使用场景:限时增益(30 秒攻击 +20%)、减速(5 秒)、冷却、任何限时状态。

// 蓝图配置:Duration Policy: Has Duration
// Duration Magnitude: 5.0
// Modifier: MoveSpeed, Multiply, 0.7(减速 30%)

5.2.3 Infinite(无限)

临时修改 CurrentValue,不会自动过期,必须手动移除。

使用场景:被动增益(装备加成)、永久减益、派生属性计算、需要手动控制的持续效果。

FActiveGameplayEffectHandle Handle = ASC->ApplyGameplayEffectSpecToSelf(Spec);
// 之后手动移除
ASC->RemoveActiveGameplayEffect(Handle);

使用场景速查

场景 推荐类型 原因
造成伤害 / 治疗 Instant 一次性永久变化
限时 Buff / 冷却 Duration 自动过期
装备加成 / 被动 Infinite 穿戴/卸下时手动控制
DOT / HOT Duration + Period 限时周期效果

常见陷阱:Instant GE 不会应用 GameplayTag,哪怕只是一帧。若需要效果生效期间触发标签逻辑,必须用 Duration 或 Infinite。

5.3 周期效果(Periodic)

Duration 和 Infinite GE 可设置 Period,每隔 X 秒重复执行一次效果。

周期执行的关键真相:

  1. 周期执行的每次触发都等同于一次 Instant 执行——它修改的是 BaseValue,而不是 CurrentValue;
  2. 周期效果不能被预测——只在服务器上执行,客户端不会有预测执行;
  3. 首次应用的 Modifier 行为:周期 GE 首次应用时,其非周期部分的 Modifier 会修改 CurrentValue(像普通 Duration/Infinite);之后每次周期执行时才修改 BaseValue。
DoT 配置:Duration=10s, Period=2s, Modifier: Health Add -15
→ 首次应用:Health CurrentValue 由聚合器体现(若有非周期部分)
→ 每 2 秒:以 Instant 方式永久扣 15 血,共 5 次
// 在 AttributeSet 中:周期效果的每次执行都进 PostGameplayEffectExecute(因为是 Instant)
void UMyAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
    if (Data.EvaluatedData.Attribute == GetHealthAttribute())
    {
        SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
        if (GetHealth() <= 0.f) { /* 死亡处理 */ }
    }
}

常见陷阱:

  • 周期效果修改 BaseValue(不是 CurrentValue);
  • 不能预测(服务器权威);
  • 首次应用的非周期 Modifier 行为与后续周期执行不同。

5.4 Modifier(修改器)

一个 GE 可配置多个 Modifier,每个 Modifier = 一个属性 + 一种运算 + 一个数值来源。

5.4.1 运算类型(Modifier Op)

运算 枚举 公式位置
加 EGameplayModOp::Additive 先执行(求和中)
乘 EGameplayModOp::Multiplicitive 加后执行
除 EGameplayModOp::Division 乘后执行
覆盖 EGameplayModOp::Override 最后,直接设最终值

5.4.2 数值来源(Modifier Magnitude)四种类型

① Scalable Float(可缩放浮点)

关联 DataTable(CurveTable)按等级取值,可再乘系数。

蓝图配置:
  Modifier Type: Scalable Float
  Scalable Float Magnitude:
    Curve Table: CT_DamageValues
    Row Name: Fireball
    Coefficient: 1.5

适用:等级成长系统、技能等级效果。

② Attribute Based(基于属性)

基于 Source 或 Target 的某个属性值计算大小。核心是快照(Snapshot):

快照 来源/目标 捕获时机 自动更新
是 Source 创建 Spec 时 否
是 Target 应用时 否
否 Source 应用时 是
否 Target 应用时 是
蓝图配置:
  Modifier Type: Attribute Based
  Attribute: AttackPower
  Attribute Source: Source
  Snapshot: true
  Coefficient: 0.8
  Pre-Additive: 10.0    // 最终 = (AttackPower + 10) * 0.8 + Post
  Post-Additive: 5.0    // 最终 = (AttackPower + 10) * 0.8 + 5

快照语义:bSnapshot=true 在 Spec 创建时捕获(GE 发出瞬间的"当时值");false 在应用时捕获且对 Duration/Infinite 会随被捕获属性变化自动更新。选错会导致"伤害打出去后攻击力变了伤害却没变"或反之。

③ Custom Calculation Class(MMC)

UGameplayModMagnitudeCalculation,最大的灵活性,可以被预测,只返回一个 float 用于单个 Modifier。

// MMC 完整示例:基于目标法力值计算毒伤害
UCLASS()
class UMMC_PoisonDamage : public UGameplayModMagnitudeCalculation
{
    GENERATED_BODY()
public:
    UMMC_PoisonDamage();
    virtual float CalculateBaseMagnitude_Implementation(
        const FGameplayEffectSpec& Spec) const override;
private:
    FGameplayEffectAttributeCaptureDefinition ManaDef;
    FGameplayEffectAttributeCaptureDefinition MaxManaDef;
};

UMMC_PoisonDamage::UMMC_PoisonDamage()
{
    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 UMMC_PoisonDamage::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.WeakToPoison"))))
        PoisonDamage *= 2.f;                          // 弱点标签再翻倍
    return PoisonDamage;
}

常见陷阱:忘记把捕获定义加入 RelevantAttributesToCapture,捕获时会报错/拿不到值。

④ Set By Caller(外部设定值)

运行时由外部(通常是 GA)通过 Tag 指定数值。

使用场景:蓄力技能(伤害随蓄力时间)、动态计算值、GA → GE 传数据。

FGameplayEffectSpecHandle Spec = MakeOutgoingGameplayEffectSpec(GE_Damage, GetAbilityLevel());
Spec.Data.Get()->SetSetByCallerMagnitude(
    FGameplayTag::RequestGameplayTag(FName("Data.Damage")), CalculatedDamage);
ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, Spec);

详见 5.9 节的 SetByCaller 完整语义。

5.5 聚合公式与执行顺序(准确版)

所有 Modifier 在 FAggregatorModChannel::EvaluateWithBase 中聚合:

FinalValue = ((BaseValue + Additive) × Multiplicitive) ÷ Division

执行顺序:

  1. 计算 Additive 总和(所有 Add 相加);
  2. 计算 Multiplicitive = 1 + Σ(Magnitudeᵢ − 1);
  3. 计算 Division = 1 + Σ(Magnitudeᵢ − 1);
  4. FinalValue = (BaseValue + Additive) × Multiplicitive ÷ Division;
  5. 若有 Override,用 Override 值替换最终值(多个 Override 取最后结果)。

百分比变化要用 Multiply:"增加 10% 攻击力"必须用 Multiply(1.1),不能用 Add——Add 在乘法之前执行,计算顺序会错。

5.6 ⚠️ Multiply 和 Divide 的陷阱(深度)

问题本质:偏移相加,不是真乘法

默认情况下,所有 Multiply/Divide 修饰器先"相对 1 的偏移量相加",再作用。这是 GAS 中最常被误解的机制之一。引擎源码:

// GameplayEffectAggregator.cpp
float FAggregatorModChannel::SumMods(
    const TArray<FAggregatorMod>& InMods, float Bias,
    const FAggregatorEvaluateParameters& Parameters)
{
    float Sum = Bias;                       // Multiply/Divide 的 Bias = 1
    for (const FAggregatorMod& Mod : InMods)
    {
        if (Mod.Qualifies())
        {
            Sum += (Mod.EvaluatedMagnitude - Bias);
        }
    }
    return Sum;
}

对 Multiply/Divide:结果 = 1 + (M1 − 1) + (M2 − 1) + ...

实际效果示例

输入的 Multiply 值 直觉期望 引擎实际 说明
1.5 1.5x 1.5x 单个值正确
1.5, 1.5 2.25x 2.0x 1 + 0.5 + 0.5 = 2
0.5 0.5x 0.5x 单个值正确
0.5, 0.5 0.25x 0x 两个 <1 直接归零!
1.1, 0.5 0.55x 0.6x 1 + 0.1 − 0.5 = 0.6

引擎隐含规则(基于 Paragon 设计)

规则:(不超过一个值 < 1) 且 (任意数量 [1, 2) 的值) 或 (一个值 >= 2)

  • 可有任意数量 [1, 2) 的值(1.1/1.2/1.3...);
  • 最多只能有一个 <1 的值(如单个 0.7 减速);
  • 多个 <1 会出错(归零)。

如需"真乘法":修改引擎

若游戏需要多个增伤真正叠乘(两个 50% 增伤 = 2.25x),需修改引擎:

// 在 FAggregatorModChannel::EvaluateWithBase 中
// 把 Multiply/Divide 的 SumMods 换成真正的连乘函数:
float Multiplicitive = MultiplyMods(Mods[EGameplayModOp::Multiplicitive], Parameters);
float Division = MultiplyMods(Mods[EGameplayModOp::Division], Parameters);

float FAggregatorModChannel::MultiplyMods(
    const TArray<FAggregatorMod>& InMods,
    const FAggregatorEvaluateParameters& Parameters)
{
    float Multiplier = 1.0f;
    for (const FAggregatorMod& Mod : InMods)
        if (Mod.Qualifies())
            Multiplier *= Mod.EvaluatedMagnitude;
    return Multiplier;
}

最佳实践:在项目初期就决定是否要改。若有多个百分比增益/减益需要叠乘,尽早改引擎代码,避免后期大规模重构。

5.7 Modifier 上的 GameplayTags(条件过滤)

每个 Modifier 可设置 SourceTags 和 TargetTags 做条件过滤。

重要规则:

  • 标签只在 GE 应用时检查一次;
  • 对周期效果,只在首次应用时检查,之后每次周期执行不再检查;
  • Attribute Based Modifier 还有 SourceTagFilter / TargetTagFilter:只有当 Source/Target 拥有对应标签时,该 Modifier 才参与计算(否则视为 0)。
蓝图配置:
  Modifier Type: Attribute Based
  Attribute: AttackPower
  Source Tag Filter: Buff.Strong
  // Source 没有 Buff.Strong → 该 Modifier 值视为 0

5.8 堆叠(Stacking)

只对 HasDuration / Infinite 类型有效,控制同一效果多次施加时的行为:

堆叠类型 行为
None 不堆叠,每次施加都是独立实例(独立计时)
AggregateBySource 按来源聚合计数,不同来源独立(多个玩家各自给 Boss 上毒)
AggregateByTarget 按目标聚合,所有来源叠加成一个实例(最多 N 层)

相关参数:

参数 说明
StackLimitCount 最大堆叠层数
StackDurationRefreshPolicy RefreshOnSuccessfulApplication(每次叠加刷新剩余时间)/ NeverRefresh
StackExpirationPolicy ClearEntireStack(到期清空)/ RemoveSingleStackAndRefreshDuration(到期只减一层并刷新时间)

经典坑:DoT 毒伤希望"每层独立计时"→ RemoveSingleStackAndRefreshDuration + NeverRefresh;希望"叠了就整体刷新"→ RefreshOnSuccessfulApplication。选错会导致"叠了 3 层却 1 层时间就结束"或"永远不结束"。

5.9 SetByCaller 详解

允许在运行时向 GESpec 附加 float,用 GameplayTag(推荐)或 FName(不推荐,易拼错、蓝图无法下拉)作键。

两种用途

用途 1:作为 Modifier(GE 中配置 SetByCaller 数值)

  • 必须在 GE 中预先定义;
  • 若 GE 定义了 SetByCaller Modifier 但 Spec 没设置对应值 → 运行时错误(ensure),返回 0;
  • 必须保证"设了就读,读前必设"。
// GE 蓝图:Modifier Type = Set By Caller, Data Tag = Data.Damage, Op = Additive
// Spec 未设置 Data.Damage 就会报错!

用途 2:通用数据传递(不预先定义)

// 设置
Spec.Data.Get()->SetSetByCallerMagnitude(
    FGameplayTag::RequestGameplayTag(FName("Data.HealAmount")), 100.f);

// 读取(可控制警告与默认值)
float Heal = Spec.GetSetByCallerMagnitude(
    FGameplayTag::RequestGameplayTag(FName("Data.HealAmount")),
    false,   // WarnIfNotFound
    0.f);    // DefaultIfNotFound

最佳实践:始终用 GameplayTag 版。FName 版容易出现拼写错误且无法在蓝图中通过下拉选择。

5.10 Gameplay Effect Spec(GESpec)详解

GESpec 是 GE 的运行时实例:GE 是蓝图模板,GESpec 是运行时创建的具体实例,可携带运行时数据。

创建方式:

// 在 GameplayAbility 中
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(EffectClass, GetAbilityLevel());

// 在 ASC 上
FGameplayEffectSpecHandle SpecHandle = ASC->MakeOutgoingSpec(EffectClass, Level, Context);

GESpec 内容:

内容 说明
Def 指向 GE 类引用
Level 等级,通常等于创建它的能力等级
Duration / Period 可覆盖 GE 默认值
StackCount 当前堆叠数
Context GameplayEffectContext(来源信息)
CapturedSourceTags / CapturedTargetTags 创建/应用时捕获的标签
DynamicGrantedTags / DynamicAssetTags 动态添加的标签
SetByCallerMagnitudes SetByCaller 值映射

传递 GESpec 给投射物(经典模式)

// GA 中创建并设置
FGameplayEffectSpecHandle Spec = MakeOutgoingGameplayEffectSpec(GE_Damage, GetAbilityLevel());
Spec.Data.Get()->SetSetByCallerMagnitude(
    FGameplayTag::RequestGameplayTag(FName("Data.Damage")), CalculatedDamage);

// 传给投射物(Deferred Spawn 保证设置生效)
AMyProjectile* Projectile = GetWorld()->SpawnActorDeferred<AMyProjectile>(ProjectileClass, SpawnTransform);
Projectile->DamageEffectSpecHandle = Spec;
Projectile->FinishSpawning(SpawnTransform);

// 投射物命中(服务端)
if (HasAuthority())
{
    UAbilitySystemComponent* TargetASC =
        UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitActor);
    if (TargetASC)
        TargetASC->ApplyGameplayEffectSpecToSelf(*DamageEffectSpecHandle.Data.Get());
}

运行时修改 Spec

Spec.Data.Get()->Duration = 10.0f;                                  // 覆盖时长
Spec.Data.Get()->DynamicAssetTags.AddTag(
    FGameplayTag::RequestGameplayTag(FName("Source.Ability.Fireball")));
Spec.Data.Get()->SetSetByCallerMagnitude(
    FGameplayTag::RequestGameplayTag(FName("Data.Damage")), 50.f);

5.11 GE 的六大标签容器

容器 作用 适用类型
Asset Tags 描述 GE 本身,不做功能(查询、RemoveActiveEffectsWithTags) 全部
Granted Tags 生效期间加给目标 ASC,移除 GE 时自动删除 Duration/Infinite
Ongoing Tag Requirements 应用后按标签条件动态开关效果(不满足则 Modifier 暂停,满足则恢复;GE 不移除) Duration/Infinite
Application Tag Requirements 应用前检查,不满足则 GE 不被应用(可查 Source 与 Target) 全部
Remove Gameplay Effects with Tags 成功应用时,移除目标上 Asset/Granted Tags 匹配的其他 GE(驱散/净化) 全部
Combined Tags Added+Removed 的结果,仅供编辑器预览,只读 —
// Ongoing 示例:护盾只在有 State.HasShield 标签时生效
// Ongoing Tag Requirements: Require Tags = State.HasShield
// 失去标签 → 护盾 Modifier 暂停;重新获得 → 恢复(GE 不被移除)

// Application 示例:免疫毒素时不上毒
// Application Tag Requirements: Ignore Tags = State.ImmuneToPoison
// 也可检查来源:Source Required Tags = Buff.EnhancedPoison

5.12 施加条件(Application Requirements)与 CAR

Tag 条件

  • Source Require / Ignore Tags:对来源要求;
  • Target Require / Ignore Tags:对目标要求(如 State.Immune.Fire)。

Custom Application Requirement(CAR)

比 Tag 更灵活的 C++ 条件检查:

UCLASS()
class UMyCustomApplicationRequirement : public UGameplayEffectCustomApplicationRequirement
{
    GENERATED_BODY()
public:
    virtual bool CanApplyGameplayEffect_Implementation(
        UGameplayEffect* GameplayEffect,
        const FGameplayEffectSpec& Spec,
        UAbilitySystemComponent* TargetASC) const override
    {
        // 例:护甲太低不应用
        float Armor = TargetASC->GetNumericAttribute(UMyAttributeSet::GetArmorAttribute());
        if (Armor < 10.f) return false;

        // 例:已达最大堆叠不应用
        FGameplayEffectQuery Query;
        Query.EffectDefinition = GameplayEffect->GetClass();
        if (TargetASC->GetActiveEffectsStackCount(Query) >= 5) return false;
        return true;
    }
};

适用:检查属性值、自定义堆叠逻辑、修改持续时间(已有实例时刷新而非新增)。

Conditional GameplayEffects

GE 可配置条件分支:施加成功/满足条件时额外触发另一个 GE(暴击额外效果、目标反应)。

5.13 Granted Abilities(授予能力)

只有 Duration 和 Infinite GE 可以授予能力(Instant 立即移除无法持有)。

三种移除策略:

策略 行为 使用场景
Cancel Immediately GE 移除时能力立即取消并移除 击飞(GE 结束强制停止)
Remove on End GE 移除时允许能力自然完成后移除 冲锋(动画播完)
Do Nothing GE 移除后能力永久保留 被动能力赋予
场景 1:击退 — Duration GE 授予 GA_Knockback(自动激活,用 RootMotionSource 移动),
        Removal Policy = Cancel Immediately → GE 到期角色停止
场景 2:被动赋予 — Infinite GE 授予 GA_LegendaryPassive,
        Removal Policy = Do Nothing → 卸下武器后能力保留,需手动 ASC->ClearAbility(Handle)

5.14 Immunity(免疫系统)

比 Application Tag Requirements 更强大,因为提供了专门回调。

两种配置方式

① GrantedApplicationImmunityTags:
   Source Tags = Character.Minion
   → 任何带 Character.Minion 标签的来源对 Boss 施放 GE 都被免疫

② Granted Application Immunity Query:
   Query Tag = Effect.Damage.Poison, Match = MatchAnyTags
   → 任何 Asset Tags 含 Effect.Damage.Poison 的 GE 都被免疫

监听免疫事件

ASC->OnImmunityBlockGameplayEffectDelegate.AddUObject(this, &AMyCharacter::OnImmunityBlock);

void AMyCharacter::OnImmunityBlock(const FGameplayEffectSpec& SpecBlocked)
{
    // 播放"免疫!"提示
    UE_LOG(LogTemp, Log, TEXT("免疫了效果:%s"), *SpecBlocked.Def->GetName());
}

场景:Boss 免疫控制(Stun/Freeze/Knockback MatchAny);护盾免疫特定伤害类型(MatchAll,只免疫 Fire)。

5.15 GameplayEffectContext(上下文)与自定义

FGameplayEffectContextHandle 携带"谁打的、怎么打的":

字段 含义
Instigator 效果发起者
EffectCauser 直接原因(投射物、AOE)
SourceObject 来源对象(GA、武器)
HitResult 碰撞命中结果
自定义扩展 子类化加暴击等字段
FGameplayEffectContextHandle Context = ASC->MakeEffectContext();
Context.AddSourceObject(this);
Context.AddHitResult(HitResult);
FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(GE_Damage, 1.f, Context);

子类化 FGameplayEffectContext(5 步)

// 1. 子类化
USTRUCT()
struct FMyGameplayEffectContext : public FGameplayEffectContext
{
    GENERATED_BODY()
public:
    UPROPERTY() float CustomDamageMultiplier = 1.0f;
    UPROPERTY() FName HitBoneName;

    // 2. GetScriptStruct
    virtual UScriptStruct* GetScriptStruct() const override
    { return FMyGameplayEffectContext::StaticStruct(); }

    // 3. Duplicate(复制自定义数据)
    virtual FGameplayEffectContext* Duplicate() const override
    {
        FMyGameplayEffectContext* NewContext = new FMyGameplayEffectContext();
        *NewContext = *this;
        NewContext->AddActors(Actors);
        if (GetHitResult()) NewContext->AddHitResult(*GetHitResult(), true);
        return NewContext;
    }

    // 4. NetSerialize(如需复制自定义数据)
    virtual bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) override
    {
        FGameplayEffectContext::NetSerialize(Ar, Map, bOutSuccess);
        Ar << CustomDamageMultiplier;
        Ar << HitBoneName;
        return true;
    }
};

// 5. TStructOpsTypeTraits
template<>
struct TStructOpsTypeTraits<FMyGameplayEffectContext>
    : public TStructOpsTypeTraitsBase2<FMyGameplayEffectContext>
{
    enum { WithNetSerializer = true, WithCopy = true };
};
// 6. AbilitySystemGlobals 指定创建者
UCLASS()
class UMyAbilitySystemGlobals : public UAbilitySystemGlobals
{
    GENERATED_BODY()
    virtual FGameplayEffectContext* AllocGameplayEffectContext() override
    { return new FMyGameplayEffectContext(); }
};

// DefaultEngine.ini
// [/Script/GameplayAbilities.AbilitySystemGlobals]
// AbilitySystemGlobalsClassName="/Script/MyGame.MyAbilitySystemGlobals"

在 ExecCalc 中读取:

FMyGameplayEffectContext* MyContext =
    static_cast<FMyGameplayEffectContext*>(Spec.GetContext().Get());
if (MyContext && MyContext->HitBoneName == FName("Head"))
    MyContext->CustomDamageMultiplier *= 2.f;   // 爆头翻倍

5.16 ExecCalc 概览(详见第 10 章)

UGameplayEffectExecutionCalculation 是最强大的 GE 修改方式:

  • 可修改多个属性;
  • 执行任意 C++ 逻辑;
  • 不能被预测(只能用于 Instant 和 Periodic GE)。

标准写法用 DECLARE_ATTRIBUTE_CAPTUREDEF / DEFINE_ATTRIBUTE_CAPTUREDEF 宏声明属性捕获(详见第 10 章),通过 OutExecutionOutput.AddOutputModifier(...) 输出结果。

5.17 向 ExecCalc 传递数据的四种方式

方式 读取 API 场景
SetByCaller Spec.GetSetByCallerMagnitude(Tag, ...) 运行时动态值(蓄力伤害)
Backing Data Attribute Calculation Modifier AttemptCalculateCapturedAttributeMagnitude(DamageDef, ...) GE 中硬编码到元属性
Temporary Variable Calculation Modifier AttemptCalculateTransientAggregatorMagnitude(Tag, ...)(构造函数 ValidTransientAggregatorIdentifiers.AddTag) 独立于属性的临时变量
GameplayEffectContext static_cast<FMyGameplayEffectContext*>(Spec.GetContext().Get()) 复杂自定义数据(命中部位)
// 方式 1:SetByCaller
float Damage = FMath::Max(
    Spec.GetSetByCallerMagnitude(
        FGameplayTag::RequestGameplayTag(FName("Data.Damage")), false, -1.f), 0.f);

// 方式 3:临时变量(构造函数中注册)
ValidTransientAggregatorIdentifiers.AddTag(FGameplayTag::RequestGameplayTag("Data.Damage"));
// ExecCalc 中读取
float Damage = 0.f;
ExecutionParams.AttemptCalculateTransientAggregatorMagnitude(
    FGameplayTag::RequestGameplayTag("Data.Damage"), EvalParams, Damage);

5.18 Cost GE 与 Cooldown GE(GA 的两件套)

Cost GE(消耗)

  • Instant GE(永久扣除),可被预测,通常每个 GA 一个;
  • 典型:Modifier: Mana, Add, -30。

复用技巧:MMC 读取 GA 上的 Cost 值(只适用于 Instanced 能力):

UCLASS()
class UMyGameplayAbility : public UGameplayAbility
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cost")
    FScalableFloat Cost;
};

// MMC 读取
float UMMC_AbilityCost::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
    const UMyGameplayAbility* Ability = Cast<UMyGameplayAbility>(
        Spec.GetContext().GetAbilityInstance_NotReplicated());
    return Ability ? Ability->Cost.GetValueAtLevel(Ability->GetAbilityLevel()) : 0.f;
}

Cooldown GE(冷却)

  • Duration GE、无 Modifier、有唯一 Cooldown Tag(GrantedTags);
  • GA 检查的是 Tag 而非 GE 本身;
  • Duration 结束 → GE 移除 → Tag 消失 → 可再次激活。
典型 Cooldown GE:
  Duration Policy: Has Duration
  Duration Magnitude: 5.0
  Modifiers: 无
  Granted Tags: Cooldown.Skill.Fireball(唯一冷却标签)

冷却与消耗在 GA 中的自动化

两者由 GA 的 GetCooldownGameplayEffect() / GetCostGameplayEffect()(或 CDO 配置)提供,在 CommitAbility() 中自动检查并施加。检查 Tag:Cooldown.Skill.Fireball 存在 → 无法再次激活。

5.19 在 C++ 中创建 GE

UGameplayEffect* GE = NewObject<UGameplayEffect>(GetTransientPackage(), TEXT("RuntimeGE"));
GE->DurationPolicy = EGameplayEffectDurationType::Instant;   // 仅 Instant 可运行时动态创建
int32 Idx = GE->Modifiers.Num();
GE->Modifiers.SetNum(Idx + 1);
FGameplayModifierInfo& Info = GE->Modifiers[Idx];
Info.Attribute = UMyAttributeSet::GetDamageAttribute();
Info.ModifierOp = EGameplayModOp::Additive;
Info.ModifierMagnitude = FScalableFloat(50.f);

重要:只有 Instant GE 能运行时从零创建。Duration/Infinite GE 因需要类定义用于复制,必须在编辑器里建资产;运行时只能"自定义 Spec"而非"新建 GE 类"。

5.20 本章验证

  • [ ] 建 Instant 伤害 GE(Meta Damage)+ HasDuration 减速 GE + Infinite 回血 GE,验证三者对 BaseValue/CurrentValue 的不同影响;
  • [ ] 用 SetByCaller 传动态伤害并成功结算;
  • [ ] 配一个带 Period 的 DoT,验证"周期执行修改 BaseValue、不可预测、首次 Modifier 行为";
  • [ ] 配一组 AggregateByTarget 堆叠并验证层数/刷新行为;
  • [ ] 用 GE 授予一个被动能力(Do Nothing)并验证移除 GE 后能力保留;
  • [ ] 配置 Immunity,用回调监听免疫事件;
  • [ ] 给角色配 Cost GE + Cooldown GE,验证 CommitAbility 自动扣蓝与冷却 Tag 阻塞再次激活;
  • [ ] 理解并能在项目里解释 Multiply 的"偏移相加"陷阱。

下一章:06 GameplayAbilities 能力系统