GAS Gameplay Effect 详解
GAS Gameplay Effect 详解
Gameplay Effect(GE)是一个继承自 UGameplayEffect 的纯数据资产。它本身不写逻辑,而是一张"指令单":描述要改哪些属性、怎么改、改多久,再交给 AbilitySystemComponent(ASC)去读取和执行。
把它当数据看,是理解整个 GAS 的关键。GE 做三件事:
- 改属性——改
AttributeSet里的浮点数(生命、魔法、攻击力等)。这是 GE 最核心的职责,也是修改属性的唯一合法通道。绕开 GE 直接写属性,变更就不可追踪、不触发回调,后面所有依赖事件的逻辑都会失效。 - 管状态——授予或移除
GameplayTag(比如挂上"眩晕"),也能授予或激活新的GameplayAbility。 - 给反馈——通过 GameplayCue 触发视觉/听觉表现(血条刷新、命中粒子、音效)。
模板、Spec 与 ASC:三层结构
GE 在运行时不是一个东西,而是三层:
UGameplayEffect模板——配置数据本身,主要由两类信息构成:FGameplayModifierInfo(属性怎么变:加、乘、覆盖)和 GameplayCue 配置(触发什么表现)。FGameplayEffectSpec实例——每次ApplyGameplayEffect都会基于模板生成一个 Spec。Spec 带着等级信息,决定这一次的具体数值。- Active Gameplay Effects Container——ASC 里管理所有已应用 Spec 的容器,负责生命周期、堆叠规则和网络同步。
所以从"配置"到"生效"的链路是这样的:


生命周期(Duration Policy)
这个设置决定 GE 怎么生效、生效多久,是 GE 的"工作模式"开关。

- Instant(即时)——生效一次立即结束,永远不进 Active Container。直接伤害、一次性治疗用它。
- Infinite(无限)——永久持续,直到被手动移除。被动技能、永久光环用它。
- Has Duration(有期限)——在设定时长内激活,结束失效。可以选周期性触发(比如每秒掉血)。持续 10 秒的攻击力 Buff、中毒用它。
选 Infinite 或 Has Duration 都会冒出 Period 选项:
Period 控制周期触发频率,0 表示不周期触发。两个例子说明区别:
| 生效方式 | 技能例子 | 实际行为 |
|---|---|---|
| Infinite + Period 5.0 | 永久诅咒:持续降属性直到特殊解除 | 每 5 秒触发一次(如力量 -1) |
| Duration 10.0 + Period 5.0 | 持续治疗 HoT | 每 5 秒回一次血,10 秒内执行 2 次 |
时长策略直接决定了改的是 Base 还是 Current:
| GE 类型 | 修饰符(Modifiers) | 执行计算(Executions) | 改 Base | 改 Current |
|---|---|---|---|---|
| Instant | 立即改 Base | 立即执行改 Base | ✓ | 随 Base 变 |
| Duration | 临时修正,影响 Current | 应用时或移除时执行 | ✗ | ✓ |
| Infinite | 临时修正,影响 Current | 应用时或移除时执行 | ✗ | ✓ |
| Periodic | 每个周期像 Instant 一样改 Base | 每周期执行一次改 Base | ✓ | 随 Base 变 |
这张表是 GE 最容易踩坑的地方:Instant/Periodic 是永久改 Base,Duration/Infinite 是临时修正 Current。后面 Attribute 详解里的"风场 Buff + 迟缓 Debuff 叠加"案例就是吃这套规则。
Gameplay Effect Components
GEComponent 是 UE5.3 引入的扩展机制,用来模块化地定制 GE 行为。它不直接参与运行时逻辑,而是作为 GE 资产内部的子对象存在,封装某一类功能的配置。
class UGameplayEffect
{
protected:
/** These Gameplay Effect Components define how this Gameplay Effect behaves when applied */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Instanced)
TArray<TObjectPtr<UGameplayEffectComponent>> GEComponents;
};

每个 GEComponent 在配置面板里的显示名就大致说明了它的用途。它的工作时机集中在编辑期和加载期,而不是运行时:
- 一个 GE 资产可以挂一个或多个 GEComponent 实例,彼此独立、互不干扰,不同 GE 之间的配置也完全隔离。
- 资产加载或蓝图编译完成时(对应 CDO 的
PostLoad()),GEComponent 会被回调,把配置数据转成 GE 的运行时缓存(比如标签集合),让运行时能 O(1) 访问。
void UGameplayEffect::PostLoad()
{
Super::PostLoad();
// 缓存 GEComponents 的数据
CachedAssetTags.Reset();
CachedGrantedTags.Reset();
CachedBlockedAbilityTags.Reset();
for (UGameplayEffectComponent* GEComponent : GEComponents)
{
if (GEComponent)
{
GEComponent->ConditionalPostLoad(); // 确保子对象完全加载
GEComponent->OnGameplayEffectChanged();
}
}
}
这套设计借了 UE 的 CDO 机制:GEComponent 的属性在 CDO 里被冻结成配置快照,加载时一次性写进缓存,运行时不再解析。好处是 GEComponent 不维护运行时状态,避免了多实例同步问题,性能和安全性都更好——它本质上是一个"面向数据的蓝图配置容器"。
配置样例:用 GEComponent 赋予冷却标签
下面这个 GE 的用途是给角色加一个持续 2 秒的 Cooldown.Dash 标签,防止冲刺被频繁触发。赋予标签这件事就是 GEComponent 提供的:

- Combined Tags——只读,展示父级继承 + 新增 - 移除算出的最终标签集。
- Add to Inherited——给当前 GE 加新标签(在继承父类标签的基础上更具体)。
- Remove from Inherited——从继承来的标签里精准裁掉某些。适合子类 GE 行为和父类略有差异时。
Remove from Inherited 这个能力在控制效果分级时特别有用。举个 RPG 里常见的场景:石化是眩晕的强化版,属于控制类,但不该被当成"普通眩晕"——有些技能只对普通眩晕生效。
- 父级
GE_Control_Stun:带Status.Control.Stun,普通眩晕(被锤子砸晕)。 - 子级
GE_Control_Petrify:继承自GE_Control_Stun,但在 Remove from Inherited 里去掉Status.Control.Stun,在 Add to Inherited 里加上Status.Control.Petrify。
效果:被石化的角色只带 Status.Control.Petrify,不带 Status.Control.Stun,所以专打普通眩晕的技能对它无效;但查询更上层 Status.Control 的通用控制技能仍然命中——这正是标签层级结构的价值。

Modifiers 与 Executions
改属性有两条路,简单的走 Modifiers,复杂的走 Executions:
| 特性 | Modifiers(修饰符) | Executions(执行计算) |
|---|---|---|
| 功能 | 对指定属性做数学运算(加/乘/覆盖/除) | 跑自定义逻辑(如攻防算最终伤害),可处理非属性逻辑 |
| 实现 | 纯数据,蓝图直接配,不写 C++ | 需继承 UGameplayEffectExecutionCalculation,写 C++ 或脚本 |
| 应用时机 | 立即应用,GE 生命周期内持续 | GE 每次即时执行或周期跳动时运行 |
| 预测 | 可预测,适合多人 | 不可预测,客户端一般算不了 |
| 作用对象 | 仅指定属性 | 一个或多个属性,也能跑非属性逻辑 |
Modifiers
| 参数 | 说明 |
|---|---|
| Attribute | 目标属性,如 Health、Armor |
| Modifier Op | Add / Multiply / Override / Divide |
| Magnitude Calculation | 数值来源:Scalable Float(直接值)/ Attribute Based(依赖其他属性)/ Set By Caller(动态传入,常用于伤害) |
| Source/Target Tag | 触发该 Modifier 的条件(可选) |
同一属性上的多个 Modifier 会按固定公式叠加:
// ((BaseValue + AddBase) * MultiplyAdditive / DivideAdditive * MultiplyCompound) + AddFinal
// 注意:对 Attribute 的修改方式受 GE Duration Policy 影响(见上面的生命周期表)

上图是一个简单治疗:应用时目标基础生命值立即永久 +20。
Executions
| 参数 | 说明 |
|---|---|
| Calculation Class | 自定义执行类,继承 UGameplayEffectExecutionCalculation |
| Conditional Gameplay Effects | 执行成功后追加应用的额外 GE |

基于护甲的伤害计算大致这么写:重写 Execute,用 ExecutionParams 捕获攻击者"力量"和目标"护甲",算出 最终伤害 = (力量 - 护甲) * 系数,再通过 OutExecutionOutput 把结果作为修饰符落到目标生命值上。GE 配 Duration Policy = Instant,Executions 选这个自定义类即可。
绑定 Gameplay Cue
GE 可以把视听反馈和自己的生命周期绑在一起。Cue 本身的概念见 Gameplay Cue Notify 详解,这里只看 GE 上的配置项。

Required Modifier Success to Trigger Cues——Cue 是否只在修饰符/执行计算真正改了属性时才触发。
true:火焰伤害 GE 绑火焰粒子。敌人可受伤 → 掉血 + 播粒子;敌人无敌 → 不掉血也不播。false:攻击技能绑"击中"音效,无论目标是否受伤都播。
Suppress Stacking Cues——仅对可堆叠 GE 有效,控制 Cue 触发频率。
true:5 层中毒,只在第一层播音效,叠满到清空期间不再播,全清后重置。false:每次叠加都播(如电击)。
Cue Parameters:
- Magnitude Attribute——把指定属性值作为强度传给 Cue,动态影响表现。
- Min/Max Level——把 GE 等级映射成 0~1 的标准化值(需 Magnitude Attribute = none)。
- Gameplay Cue Tags——指定一个或多个标签,GAS 据此找到并执行对应的 Cue Notify。
标准化强度的算法:
超出 Min/Max 会被钳到 0 或 1。例:Level=3、Min=1、Max=5 → 0.5。GAS 会把这些参数打包成 FGameplayCueParameters(含标准化强度、实际等级、施加者 Instigator、目标 Target 等),传给 Cue Notify 蓝图。
一个可升级"毒药"GE 的完整链路:GE 配 GameplayCue.Debuff.Poison + Min1/Max5 → GameplayCueNotify_Poison 监听该标签 → 在 WhileGameplayCueActive 里 Break 出 Normalized Magnitude → 喂给粒子系统的 ToxicFogDensity。于是 Level1 毒雾稀薄、Level5 浓烈、Level3 居中。
Stacking

Stacking Type 三种模式:
| None | Aggregate by Source | Aggregate by Target | |
|---|---|---|---|
| 堆叠单位 | 每次应用都是独立新实例 | 每个来源在每个目标上一个实例 | 不管多少来源,目标身上只有一个实例 |
| 独立性 | 各自独立的生命周期/计时器/修饰符 | 不同来源各自独立 | 所有来源共享一个实例和计时器 |
| 典型范例 | 单次加速/治疗 | 盗贼流血:各自独立叠加伤害 | 战士破甲:共同维持一个实例,避免过度叠加 |
这里有个反直觉的坑:None 不是"不堆叠"。它的意思是应用时不去检查目标身上有没有同名 GE,于是每次应用都新建一个实例——实际效果反而是叠了一堆。所以如果你的诉求是"身上只允许存在一个实例",最简单的做法是 Aggregate by Target,而不是 None。
堆叠相关的其余参数:
- Stack Limit Count——最大层数,到顶触发溢出策略。
- Stack Duration Refresh Policy——
Refresh on Successful Application(成功叠加刷新时长,适合破甲这类要长期维持的)/Never Refresh(计时器继续跑不刷新)。 - Stack Period Reset Policy——重新应用时是否重置周期计时器。
- Stack Expiration Policy——时长结束时
Clear Entire Stack(清光)还是Clear Only One Stack(只掉一层)。 - Overflow——叠满后:
Overflow Effects(触发额外 GE,比如叠满爆炸)、Deny Overflow Application(拒绝刷新,锁定堆叠)、Clear Stack on Overflow(溢出后清光)。
团队流血 + 破甲的配合是个经典示范:流血用 Aggregate by Source(两个盗贼 = 两个独立流血实例,各叠 5 层,伤害累加),破甲用 Aggregate by Target(不管几个战士,Boss 身上只有一个破甲实例累积层数,还顺便避免了低级破甲覆盖高级)。具体选哪种由玩法决定,GE 把常见情况都兜住了。
几条实践经验
单一职责。 别造"万能 GE"——一个 GE 既扣血又加移速还附带眩晕。耦合高、难复用、出问题难定位。拆成 GE_ArrowDamage(即时伤害)+ GE_PoisonOverTime(周期毒),再用 Gameplay Ability 把它们按顺序/条件组合起来,逻辑清楚得多。
Modifiers 优先于 Executions。 Modifiers 纯数据驱动、开销极低,简单增减乘覆盖都用它。Executions 要跑代码,开销看复杂度——尤其别在频繁跳动的周期 GE 里塞重计算,那是会成倍放大的。即时 GE 用 Execution 影响有限。
服务器权威。 所有 GE 的实际应用和属性修改必须在服务器完成。在客户端直接 Apply 或改属性会导致状态不一致,在多人里是严重 bug。需要即时反馈时走预测系统(可预测的 GE 配合 Wait for Confirmation 让客户端先预测、服务器再确认),但依赖服务器数据的复杂 Execution 不适合预测。
用标签做控制和免疫。 GE 激活时授予标签、移除时收回,这些标签可用于查询、能力封锁和免疫。比如一个能力可以配成"目标带 Effect.State.Stunned 时无法施放";免疫则用 Effect Tag Query——GE_PoisonImmunity 查 Effect.Debuff.Poison 来挡中毒,GE_Cure 查所有 Effect.Negative 标签的 GE 一次性移除。