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 的配置(
UGameplayEffectCDO)+ 运行时参数(等级、数值、上下文)组装成一份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 秒重复执行一次效果。
周期执行的关键真相:
- 周期执行的每次触发都等同于一次 Instant 执行——它修改的是 BaseValue,而不是 CurrentValue;
- 周期效果不能被预测——只在服务器上执行,客户端不会有预测执行;
- 首次应用的 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
执行顺序:
- 计算 Additive 总和(所有 Add 相加);
- 计算 Multiplicitive = 1 + Σ(Magnitudeᵢ − 1);
- 计算 Division = 1 + Σ(Magnitudeᵢ − 1);
- FinalValue = (BaseValue + Additive) × Multiplicitive ÷ Division;
- 若有 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 的"偏移相加"陷阱。