UE5 GAS 源码深度解析 | 第3篇:GameplayEffect 源码导读(上)
前言
第 2 篇把 AttributeSet 的数据结构讲完,属性值在内存里怎么存、怎么改、怎么同步已经清楚。但"谁在改、怎么改、改完怎么管"这个问题还没回答——这正是 GameplayEffect(GE)的职责。
上一篇文末预告的是 ASC,但效果这东西改属性、挂 Buff、管持续时间,几乎把 GAS 里"改数"这条线的核心逻辑都串起来了。理解了 GE 的源码,后面再读 ASC 的管理、网络预测、Stacking 会省不少力气,所以这篇先把 GameplayEffect 交代清楚。
GE 在架构里的角色
回顾 第 1 篇的图 2,GE 处在"修改层":负责"改属性和改状态"的原子操作。一个 GE 可以配置若干 Modifier(改哪个属性、怎么改、改多少),也可以改 GameplayTag(加/减状态标签),还可以授予技能、触发条件效果等。
这篇聚焦 GE 改属性的核心逻辑——Modifier 机制、GE 的静态定义、运行时实例(Spec)、DurationPolicy(Instant/Duration/Infinite)。Stacking、ExecutionCalculation、GE 组件(Tags、ConditionalEffects、GrantedAbilities)这些高级特性留给第 4 篇。
版本说明
- 文本文依旧是以 Unreal Engine 5.7 插件源码为准,和你本地不一致的地方以你手里的代码为准。
- 主要会翻:
Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayEffect.hEngine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayEffect.cppEngine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayEffectSpec.hEngine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayEffectSpec.cppAbilitySystemComponent.h/AbilitySystemComponent.cpp(ApplyGameplayEffectSpecToSelf、ExecuteActiveEffects 等)GameplayEffectModifierMagnitude.h/GameplayEffectModifierMagnitude.cpp(三种 Magnitude 的分支)AttributeSet.cpp(FAggregator::AddModifier、FAggregator::ExecuteMod)GameplayEffectAttributeCaptureSpec.h/GameplayEffectAttributeCaptureSpec.cpp(Attribute Capture)
类名、函数名建议在上述文件里搜一下,边看边对。
从 Modifier 开始:改数的基本单元
在深入 UGameplayEffect 之前,先把改数的基本单元对齐——Modifier。一个 GE 可以有多个 Modifier,每个 Modifier 负责"改一个属性"。
FGameplayModifierInfo:配置时的结构
FGameplayModifierInfo 是 GE 里配置 Modifier 的结构,核心字段:
Attribute:要改哪个属性(FGameplayAttribute句柄)ModifierOp:怎么改(EGameplayModOp::Type枚举)ModifierMagnitude:改多少(FGameplayEffectModifierMagnitude)
ModifierOp 的运算语义和执行顺序,源码里的注释写得很清楚:
1 | // GameplayEffectTypes.h |
六种运算的执行顺序:
- AddBase:先执行,加到 BaseValue 上
- MultiplyAdditive:乘数先加在一起,再乘
- DivideAdditive:除数先加在一起,再除
- MultiplyCompound:连续相乘
- AddFinal:最后加
- Override:直接覆盖(慎用)
完整公式:((BaseValue + AddBase) * MultiplyAdditive / DivideAdditive * MultiplyCompound) + AddFinal
举个例子:BaseValue = 100,有 3 个 Modifier(AddBase +10、MultiplyAdditive 0.5、AddFinal +20)
- 先算 AddBase:
100 + 10 = 110 - 再算 MultiplyAdditive:如果有两个乘数 0.5 和 0.3,先加起来
0.5 + 0.3 = 0.8,再乘110 * 0.8 = 88 - 最后算 AddFinal:
88 + 20 = 108
你可能会觉得 6 种 Op 有点多,其实这是为了支持不同的运算需求:AddBase 改基准值,MultiplyAdditive/DivideAdditive 做百分比加成,MultiplyCompound 做连乘(比如"增加 50% 伤害"两个 Buff 叠加是 1.5 × 1.5 = 2.25),AddFinal 做最后修正,Override 直接覆盖。理解了运算顺序,后面看聚合器的源码就顺了。
概念本身不玄,重点在 ModifierMagnitude 怎么算——这是 Modifier 的核心。
ModifierMagnitude 的四种计算方式
FGameplayEffectModifierMagnitude 支持四种计算方式,源码里用 EGameplayEffectMagnitudeCalculation 枚举区分:
1 | // GameplayEffect.h |
Scalable Float:CurveTable 查表 + Level 缩放
最常用的一种。FScalableFloat 可以直接指定数值,也可以从 CurveTable 查表:
1 | // Engine/Source/Runtime/GameplayTags/Classes/GameplayTagContainer.h |
设计意图:让策划配一张表,不同 Level 对应不同数值。比如"技能等级 1 → +10 攻击力,等级 2 → +20 攻击力",GE 施加时传入 Level,自动查表得到数值。
调用时机:Spec 创建时调用 AttemptCalculateMagnitude,得到 EvaluatedMagnitude(已计算的数值),后续运算直接用这个值。
Attribute Based:从另一个属性取值
从 Source 或 Target 的 ASC 上捕获另一个属性的值,支持多种计算策略。FAttributeBasedFloat 的源码:
1 | // GameplayEffect.h |
EAttributeBasedFloatCalculationType 四种策略:
AttributeMagnitude:用最终计算值(Current)AttributeBaseValue:用 Base 值AttributeBonusMagnitude:用 Bonus 值(Current - Base)AttributeMagnitudeEvaluatedUpToChannel:算到指定 Channel 为止
设计意图:让 Modifier 的数值动态依赖另一个属性。比如"Buff 强度 = 目标当前生命值 × 0.1",目标血量变化时,Buff 强度跟着变。
Capture 的时机:Spec 创建时捕获(Snapshot)或运算时实时取值(Non-Snapshot),由 BackingAttribute 的 Snapshot 字段决定。
Custom Calculation:自定义计算逻辑
最灵活的一种,继承 UGameplayModMagnitudeCalculation,在蓝图或 C++ 里写自定义逻辑:
1 | // GameplayModMagnitudeCalculation.h |
设计意图:当 Scalable Float 和 Attribute Based 都不够用时,用自定义计算。比如"根据目标身上已有的 Buff 数量决定强度"、"根据距离衰减"等复杂逻辑。
调用时机:Spec 创建时调用 CalculateBaseMagnitude,算出的值还会被 FCustomCalculationBasedFloat 的 Coefficient、PreMultiplyAdditiveValue、PostMultiplyAdditiveValue 进一步处理。
SetByCaller:由调用者设置
运行时由代码或蓝图设置数值,不在 GE 里配置:
1 | // GameplayEffect.h |
设计意图:让运行时决定数值。比如"技能伤害 = 武器伤害 × 技能倍率",武器伤害在运行时从武器 Actor 上取,不在 GE 里写死。
使用方式:在创建 Spec 时调用 SetSetByCallerMagnitude 设置数值,Spec 创建后 Magnitude 计算时会直接用这个值。
四种方式里,ScalableFloat 和 AttributeBased 最常用,CustomCalculation 适合复杂逻辑,SetByCaller 适合运行时传值。选哪种取决于你的数值从哪来:配表用 ScalableFloat,依赖属性用 AttributeBased,复杂计算用 CustomCalculation,运行时传值用 SetByCaller。
对比四种方式的源码路径
FGameplayEffectModifierMagnitude::AttemptCalculateMagnitude 的核心分支:
1 | // GameplayEffect.cpp |
flowchart TB
Spec["FGameplayEffectSpec"] --> Magnitude["AttemptCalculateMagnitude"]
Magnitude --> Type{MagnitudeCalculationType}
Type -->|ScalableFloat| Curve["ScalableFloat\n或 CurveTable 查表"]
Type -->|AttributeBased| Capture["捕获属性值\n× Coefficient"]
Type -->|CustomCalculationClass| Custom["自定义计算类\nCalculateBaseMagnitude"]
Type -->|SetByCaller| SetByCaller["从 Spec 取\nSetByCaller 数值"]
Curve --> Eval["EvaluatedMagnitude\n(已计算的数值)"]
Capture --> Eval
Custom --> Eval
SetByCaller --> Eval
图 1:四种 Magnitude 计算方式都在 Spec 创建时调用,得到 EvaluatedMagnitude。
Modifier 在 Spec 中的激活
GE 是静态配置,真正施加到 ASC 上的是 FGameplayEffectSpec(运行时实例)。Spec 创建时会把 GE 的每个 FGameplayModifierInfo 对应起来,但运行时的数据结构比配置时简单。
FModifierSpec:运行时的 Modifier
1 | // GameplayEffect.h |
关键:FModifierSpec 只存 EvaluatedMagnitude,不存 ModifierInfo 或 ModifierHandle。你会发现运行时结构比配置时简单很多:配置时要存属性、Op、Magnitude 计算方式等一堆信息,运行时只剩一个算好的数值。这是因为 Spec 创建时就已经把 Magnitude 算好了,后续运算只需要这个值。
Spec 如何持有 Modifier 信息:
1 | // GameplayEffect.h (简化) |
创建过程:Spec 构造时遍历 Def->Modifiers,对每个 FGameplayModifierInfo 调用 ModifierMagnitude.AttemptCalculateMagnitude,算出 EvaluatedMagnitude,存入 Modifiers 数组。
聚合器绑定:Modifier 如何挂到 ASC 上
Duration / Infinite 的 GE 会把 Modifier 挂到聚合器(Aggregator)上。聚合器是每个属性上的"运算中枢",维护所有生效的 Modifier,求值时把 Base 和各条 Modifier 按规则合成 Current。
绑定发生在 FActiveGameplayEffectsContainer::AddModifierToAggregator:
1 | // AbilitySystemComponent.cpp (简化) |
ModifierHandles 的作用:FActiveGameplayEffect 维护一个 TArray<int32> ModifierHandles,存每个 Modifier 在聚合器里的索引。移除 GE 时,用这些索引从聚合器移除对应 Modifier。
flowchart LR
GE["UGameplayEffect\n(静态配置)"] -->|创建| Spec["FGameplayEffectSpec"]
Spec -->|实例化| ModSpecs["TArray<FModifierSpec>\n(只有 EvaluatedMagnitude)"]
ModSpecs -->|AddModifierToAggregator| Aggregator["FAggregator\n(聚合器)"]
Aggregator -->|返回索引| Handles["FActiveGameplayEffect\n.ModifierHandles"]
图 2:从 GE 到 Spec 到 ModifierSpec,再挂到聚合器上,索引存到 ActiveGE.ModifierHandles。
运算调用链:从 Modifier 到 Current Value
Modifier 挂到聚合器后,每次属性求值都会调用聚合器的运算逻辑,把 Base 和所有 Modifier 合成 Current。
Modifier 的执行顺序
聚合器求值时,按 EGameplayModOp 的顺序执行,不是按添加顺序。源码里用 FAggregatorModChannel::ExecuteMod 实现:
1 | // GameplayEffectAggregator.cpp (简化) |
执行顺序:
- 所有 AddBase 先加在一起
- 所有 MultiplyAdditive 先加在一起,再乘
- 所有 DivideAdditive 先加在一起,再除
- 所有 MultiplyCompound 连续相乘
- 所有 AddFinal 先加在一起
- Override 取最后一个
这里有个细节:为什么 AddBase/MultiplyAdditive/DivideAdditive/AddFinal 同类 Op 先聚合再运算,而 MultiplyCompound/Override 是逐个执行?因为前者的运算顺序不影响结果(加法交换律、乘法交换律),后者会受顺序影响。源码这样设计,是为了让多个同类 Modifier 的效果可预期、不依赖添加顺序。
为什么这样设计:AddBase/MultiplyAdditive/DivideAdditive/AddFinal 同类 Op 先聚合再运算,可以保证运算顺序一致;MultiplyCompound 和 Override 是逐个执行,结果依赖顺序。
flowchart TB
Base["BaseValue"] --> AddBase["+ 所有 AddBase\n先加在一起"]
AddBase --> Multiply["× 所有 MultiplyAdditive\n先加在一起再乘"]
Multiply --> Divide["÷ 所有 DivideAdditive\n先加在一起再除"]
Divide --> Compound["× 所有 MultiplyCompound\n连续相乘"]
Compound --> AddFinal["+ 所有 AddFinal\n先加在一起"]
AddFinal --> Override["Override\n取最后一个"]
Override --> Current["CurrentValue"]
subgraph Mods["聚合器上的 Modifier 列表"]
M1["Modifier 1 (AddBase)"]
M2["Modifier 2 (MultiplyAdditive)"]
M3["Modifier 3 (DivideAdditive)"]
M4["Modifier 4 (MultiplyCompound)"]
M5["Modifier 5 (AddFinal)"]
M6["Modifier 6 (Override)"]
end
图 3:ExecuteMod 按 Op 类型分组执行,同类 Op 先聚合再运算。
完整调用链
从 GE Apply 到 Current Value 的完整路径:
sequenceDiagram
participant ASC as AbilitySystemComponent
participant Spec as FGameplayEffectSpec
participant ActiveGE as FActiveGameplayEffect
participant Agg as FAggregator
participant Attr as AttributeSet
ASC->>Spec: 创建 Spec
Spec->>Spec: AttemptCalculateMagnitude<br/>(算出 EvaluatedMagnitude)
ASC->>ActiveGE: 创建 ActiveGE
ActiveGE->>Agg: AddModifierToAggregator
Agg->>Agg: 挂到 Modifiers 列表
Note over ASC: 属性求值时
ASC->>Agg: ExecuteMod
Agg->>Attr: 读 BaseValue
Agg->>Agg: 按 Op 顺序运算
Agg->>Attr: 写 CurrentValue
图 4:从 GE Apply 到 Current Value 的完整调用链。
最小化实例:+10 Attack Buff
构造一个最简单的 Duration GE,只有一个 Modifier:
1 | // GE_AttackBuff |
执行流程:
ApplyGameplayEffectSpecToSelf:创建 SpecAttemptCalculateMagnitude:EvaluatedMagnitude = 10.fAddModifierToAggregator:挂到 Attack 属性的聚合器上ExecuteMod:Result = BaseValue + 10- 10 秒后
RemoveActiveGameplayEffect:从聚合器移除 Modifier,Current 恢复到 Base
这个实例覆盖了 Modifier 的核心路径:创建 → 激活 → 运算 → 移除。后面讲 DurationType 时会对比 Instant vs HasDuration 的差异。
UGameplayEffect:静态配置与蓝图友好
改数的基本单元搞清楚了,现在回头看 UGameplayEffect 如何配置这些 Modifier。
UGameplayEffect 的核心字段
UGameplayEffect 是 Blueprintable 的 UObject,核心字段:
基础字段:
Modifiers:TArray<FGameplayModifierInfo>,Modifier 列表DurationPolicy:EGameplayEffectDurationType,Instant / Infinite / HasDuration(下一节深挖)DurationMagnitude:FGameplayEffectModifierMagnitude,持续时间(秒)Period:周期执行(用于 DoT 类 GE,每隔几秒触发一次)
高级字段(一笔带过,预告第 4 篇):
StackingType/StackLimit/StackDurationRefreshPolicy:堆叠机制(第 4 篇)GrantedTags/AddedTags/RemovedTags:Tag 管理(第 4 篇)ConditionalGameplayEffects:条件触发(第 4 篇)GrantedAbilities:授予技能(第 4 篇)Executions:TArray<FGameplayEffectExecutionDefinition>,ExecutionCalculation(第 4 篇)
概念本身不玄,重点是 DurationPolicy——它决定了 GE 的执行语义和生命周期。
DurationType:Instant / Infinite / HasDuration 的实现差异
EGameplayEffectDurationType 枚举:
1 | // GameplayEffect.h |
三种 Type 在源码里的实现路径完全不同。
Instant:立即执行、不挂聚合器
语义:立即执行一次、不挂聚合器、直接改 Base 或走 Execution。
源码路径:ApplyGameplayEffectSpecToSelf 中 Instant 的分支:
1 | // AbilitySystemComponent.cpp (简化) |
关键:Instant GE 不会留在 ASC 的 ActiveEffects 列表里,执行完就没了。
ExecuteActiveEffects:遍历 Spec 的所有 Modifier,立即修改 BaseValue:
1 | // AbilitySystemComponent.cpp (简化) |
对比 Infinite/HasDuration:Instant 不挂聚合器,不会参与后续的 Current 求值,直接改 Base。
HasDuration:挂聚合器、持续一段时间、定时移除
语义:挂聚合器、持续一段时间、定时移除。
源码路径:ApplyGameplayEffectSpecToSelf 中 HasDuration 的分支:
1 | // AbilitySystemComponent.cpp (简化) |
Duration 字段的计算:DurationMagnitude 是 FGameplayEffectModifierMagnitude 类型,支持四种计算方式(ScalableFloat、AttributeBased、CustomCalculation、SetByCaller),和 Modifier 的 Magnitude 一样。
定时移除:CheckActiveGameplayEffects(每帧调用):
1 | // AbilitySystemComponent.cpp (简化) |
移除逻辑:从聚合器移除 Modifier,Current 恢复到 Base:
1 | // AbilitySystemComponent.cpp (简化) |
Infinite:挂聚合器、手动移除、无 Duration
语义:挂聚合器、持续到手动移除、无 Duration。
源码路径:和 HasDuration 的分支一样,只是 Duration = -1,没有 EndServerWorldTime:
1 | // AbilitySystemComponent.cpp (简化) |
手动移除:调用 RemoveActiveGameplayEffect,和 HasDuration 移除的逻辑一致。
对比三种 Type 的源码分支
flowchart TB
Apply["ApplyGameplayEffectSpecToSelf"] --> Type{DurationPolicy}
Type -->|Instant| Exec["ExecuteActiveEffects\n立即执行"]
Exec --> Done["执行完结束\n不创建 ActiveGE"]
Type -->|HasDuration| Create["创建 FActiveGameplayEffect"]
Create --> Duration["计算 Duration\nEndServerWorldTime"]
Duration --> Add["挂聚合器"]
Add --> List["添加到 ActiveEffects"]
List --> Check["每帧 CheckActiveGameplayEffects"]
Check --> Remove["EndServerWorldTime 到了 → RemoveActiveGameplayEffect"]
Type -->|Infinite| Create2["创建 FActiveGameplayEffect\nDuration = -1"]
Create2 --> Add2["挂聚合器"]
Add2 --> List2["添加到 ActiveEffects"]
List2 --> Manual["手动调用 RemoveActiveGameplayEffect"]
图 5:三种 DurationType 的源码分支。Instant 立即执行、HasDuration 定时移除、Infinite 手动移除。
GE 的蓝图可配置性
UGameplayEffect 是 Blueprintable,设计意图:让策划配置 GE、不写 C++。
Blueprintable 的好处:
- 策划可以在蓝图编辑器里配置 Modifier(选属性、选 Op、配数值)
- 可以配置 Duration、Period、Stacking、Tags 等参数
- 可以继承 GE 蓝图,做变体(比如"火球术伤害 GE" → “大火球术伤害 GE”)
强调:GE 是"静态配置",Spec 才是"运行时实例"。同一个 GE 蓝图可以施加多次,每次都是独立的 Spec 和 ActiveGE。
Instant vs HasDuration 对比实例
构造两个 GE,对比 Apply 路径差异:
GE_Instant:
1 | DurationPolicy = EGameplayEffectDurationType::Instant; |
GE_HasDuration:
1 | DurationPolicy = EGameplayEffectDurationType::HasDuration; |
Apply 路径对比:
| 步骤 | GE_Instant | GE_HasDuration |
|---|---|---|
| Apply | ExecuteActiveEffects | 创建 ActiveGE |
| Modifier | 直接改 BaseValue(+10) | 挂聚合器(Current = Base + 10) |
| ActiveEffects | 不加入列表 | 加入列表,有 Handle |
| 移除 | 无(执行完就没了) | 10秒后 CheckActiveGameplayEffects 移除 |
| 移除后 | Base 已改,不回退 | 从聚合器移除,Current 恢复到 Base |
关键差异:
- Instant 改 BaseValue,HasDuration 改 Current(挂聚合器)
- Instant 不留在 ActiveEffects,HasDuration 会留到时间结束
- HasDuration 移除后 Current 会回退,Instant 改的 BaseValue 是永久性的(除非手动改回去)
理解这个差异很重要,因为它直接影响你怎么设计 Buff、永久增益、消耗品这类效果。Instant 适合"永久改动",HasDuration 适合"临时 Buff"。后面第 4 篇讲网络预测时还会提到:Instant GE 客户端预测执行后,服务器对账相对简单;HasDuration GE 客户端挂聚合器,服务器移除时对账要考虑更多情况。
FGameplayEffectSpec:运行时实例
UGameplayEffect 是静态配置,真正施加到 ASC 上的是 FGameplayEffectSpec(运行时实例)。
Spec 的创建:从 GE CDO 到运行时实例
FGameplayEffectSpec::Create 是创建 Spec 的入口:
1 | // GameplayEffectSpec.cpp |
三个关键步骤:
- 从 GE CDO 复制字段:
Def指向 GE 的 CDO(Class Default Object) - 实例化 ModifierSpecs:调用
FModifierSpec构造,算出EvaluatedMagnitude - 初始化运行时状态:Handle、Duration 等
Spec 持有的运行时状态
1 | // GameplayEffectSpec.h |
关键字段:
Handle:FActiveGameplayEffectHandle(唯一标识,用于后续管理)Level:当前 Level(用于 Scalable Float 的 Level 缩放)Duration:实际持续时间(从 GE 的DurationMagnitude+ Context 计算)EffectContext:施放者、来源对象、技能等上下文信息
ModifierSpecs 的实例化
FModifierSpec 的构造:
1 | // GameplayEffectSpec.cpp |
关键:CalculateMagnitude 在构造时调用,算出 EvaluatedMagnitude。这个值在后续运算中不会再变(除非 GE 本身配置了 Dynamic Magnitude)。
Spec 的上下文:FGameplayEffectContext
FGameplayEffectContext 承载 GE 施加时的上下文信息:
1 | // GameplayEffectContext.h |
Context 的用途:
Instigator/SourceActor:用于 Attribute Capture(从 Source 的 ASC 捕获属性)AbilitySpecHandle:用于 ExecutionCalculation(拿到触发 GE 的技能)SourceTags/TargetTags:用于条件判断(比如"目标有免疫 Tag 则不施加")
Context 如何在 Spec 创建时传入:
1 | // AbilitySystemComponent.cpp |
Context 如何在 Modifier 运算时使用:
Attribute Based Magnitude 的 Capture:
1 | // GameplayEffectModifierMagnitude.cpp |
Attribute Capture:从 Source/Target 的 ASC 捕获属性值
FGameplayEffectAttributeCaptureSpec 的设计意图:在 Spec 创建时捕获 Source/Target 的属性值,供后续运算使用。
1 | // GameplayEffectAttributeCaptureSpec.h |
Capture 的时机:
- Snapshot(bSnapshot = true):Spec 创建时立即捕获,后续运算用这个快照值
- Non-Snapshot(bSnapshot = false):运算时实时从 Source/Target 的 ASC 取值
Snapshot vs Non-Snapshot 的差异(预告第 4 篇):
| 类型 | Snapshot | Non-Snapshot |
|---|---|---|
| 捕获时机 | Spec 创建时 | 运算时 |
| 数值稳定性 | 固定不变 | 实时变化 |
| 网络预测 | 客户端和服务器用同一个快照 | 客户端和服务器可能取到不同的值 |
| 适用场景 | “技能释放时的属性值” | “目标当前的属性值” |
第 4 篇讲网络预测时会展开 Snapshot vs Non-Snapshot 的对账问题。
Spec 与 ActiveGameplayEffect:ASC 的生命周期管理
FActiveGameplayEffect 是 ASC 包装 Spec 的结构:
1 | // AbilitySystemComponent.h |
ASC 如何管理 ActiveGE:
1 | // AbilitySystemComponent.h |
Duration GE 的剩余时间计算:
1 | // AbilitySystemComponent.cpp |
定时移除:CheckActiveGameplayEffects(每帧调用):
1 | // AbilitySystemComponent.cpp |
不展开:ActiveGE 的详细管理逻辑(Stacking、网络同步)留给第 4 篇。
追踪 Duration GE 的完整路径
从 Apply → Spec 创建 → ActiveGE 包装 → ASC 管理 → Remove:
sequenceDiagram
participant GA as GameplayAbility
participant ASC as AbilitySystemComponent
participant Spec as FGameplayEffectSpec
participant ActiveGE as FActiveGameplayEffect
participant Agg as FAggregator
participant Attr as AttributeSet
GA->>ASC: ApplyGameplayEffectSpecToSelf
ASC->>Spec: FGameplayEffectSpec::Create(GE, Context, Level)
Spec->>Spec: 实例化 ModifierSpecs(CalculateMagnitude)
ASC->>ActiveGE: 创建 FActiveGameplayEffect(Spec)
ActiveGE->>ActiveGE: 计算 StartWorldTime / EndWorldTime
ActiveGE->>Agg: AddModifier(返回 Handle)
Agg->>Attr: 挂到聚合器
ASC->>ASC: ActiveEffects.Add(ActiveGE)
Note over ASC: 每帧 CheckActiveGameplayEffects
ASC->>ActiveGE: GetRemainingTime
ActiveGE->>ASC: EndWorldTime <= CurrentTime → RemoveActiveGameplayEffect
ASC->>Agg: RemoveModifier(Handle)
Agg->>Attr: 从聚合器移除,Current 恢复到 Base
ASC->>ASC: ActiveEffects.Remove(ActiveGE)
图 6:Duration GE 从 Apply 到 Remove 的完整路径。
GE CDO → Spec → ActiveGE → ASC 管理的流程图
flowchart TB
subgraph Config["配置层"]
GE["UGameplayEffect CDO\n(静态配置)"]
Modifier["Modifiers 列表\n(ModifierInfo)"]
end
subgraph Runtime["运行时实例"]
Spec["FGameplayEffectSpec\n(Create 时实例化)"]
ModSpec["FModifierSpec\n(EvaluatedMagnitude)"]
Context["FGameplayEffectContext\n(Instigator / Source)"]
end
subgraph Management["ASC 管理"]
ActiveGE["FActiveGameplayEffect\n(包装 Spec)"]
Handle["FModifierHandle\n(聚合器句柄)"]
end
subgraph Aggregator["聚合器"]
Agg["FAggregator\n(运算中枢)"]
ModList["Modifier 列表\n(挂载)"]
end
GE --> Spec
Modifier --> ModSpec
Context --> Spec
Spec --> ActiveGE
ModSpec --> Handle
Handle --> ModList
ActiveGE --> ASC["ASC.ActiveEffects 列表"]
Agg --> Attr["AttributeSet\n(属性值)"]
ModList --> Agg
ASC -->|每帧 Check| Remove["RemoveActiveGameplayEffect"]
Remove -->|RemoveModifier| ModList
图 7:从 GE CDO 到 Spec 到 ActiveGE,再到 ASC 和聚合器的完整流程。
小结
这篇从 Modifier 的"改数基本单元"开始,到 GE 的静态配置、运行时实例、DurationPolicy 的实现差异,把 GameplayEffect 改属性的核心逻辑串了一遍。总结以下要点:
-
Modifier:改数的基本单元
- Magnitude 的三种计算方式:Scalable Float(CurveTable 查表 + Level 缩放)、Attribute Based(从 Source/Target 捕获属性值)、Custom Calculation(自定义逻辑)
- 聚合器绑定:
AddModifier挂到聚合器上,返回FModifierHandle - 运算调用链:
ExecuteMod→ Base + Modifier → Current(Op 优先级:Multiply → Add → Override)
-
GE 定义:静态配置
- 核心字段:
Modifiers、DurationPolicy、Duration、Period - Blueprintable 的设计意图:让策划配置 GE、不写 C++
- 强调:GE 是"静态配置",Spec 才是"运行时实例"
- 核心字段:
-
Spec:运行时实例
- 创建:
FGameplayEffectSpec::Create→ 从 GE CDO 复制字段 → 实例化 ModifierSpecs - 上下文:
FGameplayEffectContext(Instigator、SourceObject、Ability 等) - Attribute Capture:Snapshot(创建时捕获)vs Non-Snapshot(运算时取值)
- 状态:
Handle、Level、Duration、StartWorldTime
- 创建:
-
DurationPolicy:Instant / Duration / Infinite 的实现差异
- Instant:立即执行、不挂聚合器、直接改 Base、不留在 ActiveEffects
- Duration:挂聚合器、持续一段时间、定时移除(CheckActiveGameplayEffects)
- Infinite:挂聚合器、手动移除、Duration = -1
阅读建议
建议读者在源码中追踪以下路径,对照最小化 GE 实例调试:
-
Modifier 的创建与运算:
FGameplayEffectSpec::Create→FModifierSpec构造 →CalculateMagnitudeFAggregator::AddModifier→ExecuteMod
-
DurationPolicy 的分支:
ApplyGameplayEffectSpecToSelf→ switchDurationPolicy- Instant:
ExecuteActiveEffects→ 直接改 Base - Duration:创建 ActiveGE → 挂聚合器 →
CheckActiveGameplayEffects→ 移除
-
对比 Instant vs Duration:
- 构造两个 GE(GE_Instant 和 GE_Duration),Modifier 配置一样
- Apply 后观察:Instant 改 Base、Duration 改 Current
- Duration 移除后观察:Current 恢复到 Base
-
追踪 Attribute Based Magnitude:
- 配置一个"强度 = 目标当前生命值 × 0.1"的 Modifier
- 在
FAttributeBasedFloat::Calculate里打断点,观察 Capture 的过程
收尾
好了,以上就是 GameplayEffect 源码导读的第一部分,从 Modifier 的改数机制到 GE 的静态配置、运行时实例、DurationPolicy 的实现差异。改属性这条线的核心逻辑已经串清楚,后面再读 ASC 的管理、网络预测、Stacking 就有基础了。
下一篇会把 GameplayEffect 的高级特性讲完:
- Stacking 机制:堆叠计数、刷新策略(Refresh / Reset)、堆叠上限、堆叠对 Modifier 运算的影响(StackCount 如何参与运算)
- ExecutionCalculation:自定义执行逻辑、Exec 与 Modifier 的协作、Exec 的调用时机、Exec 如何写输出(AddOutputModifier)
- GE 组件:Tags(Granted / Added / Removed)、ConditionalGameplayEffects(条件触发)、GrantedAbilities(授予技能)、免疫与免疫 Tag
我们下篇见。



