# GAS
## 思考
关于 GE (Gameplay Effect) 有一些想法
一个 Effect 需要考虑哪些功能呢?
- 配置项
- 周期触发配置项
- 叠层配置项
- 冲突、免疫配置项
- 冲突:该 Effect 生效时,哪些已有的 Buff 会失去效果
- 免疫:该 Effect 生效时,哪些还未添加的 Buff 无法添加或不能生效
- 上下文信息
- Caster:施加者。可以为空,比如场地效果
- Parent:被施加者。不可为空
- Ability:通过 技能 施加。可以为空,比如场地效果,以 技能 形式添加
- 当前 Effect 的 Tag
- 当前 Effect 所处的状态
- BeforeInit
- Init
- Exeution
- Finished
- Destroy
- Caster 被销毁时,是否保持该 Effect
- Parent 被销毁时,是否保持该 Effect
- 对属性进行的操作
- 是否在 UI 上显示
- 阶段触发事件
- 通知 Parent/Caster/Ability 当前 Buff 所处阶段
- 操作配置
- 优先级
- 目标属性
- 操作配置:加、乘、除、覆盖
- 数值配置
- 触发周期配置
- 触发类型
- 直接触发
- 周期性触发,持续一段时间
- 周期性触发,永久存在
- 触发周期间隔时间
- 持续时间
- 是否立刻触发:如果是周期性触发,是否在生效时立刻触发一次
- 叠层机制配置
- 能否叠层
- 最大叠层数限制
- 叠层方案
- 不叠层,刷新当前 buff 的持续时间
- 叠层,不刷新当前 buff 的持续时间,一层一层减少
- 叠层,刷新当前 buff的持续时间,并且更改数值,一层一层减少
- 叠层,刷新当前 buff 的持续时间,并且更改数值,一次性扣完所有层数
- 数值配置
- 配置固定值
- 基于属性百分比
- 数值来源
- Caster
- Parent
- 是否限制取值范围
- 最大值
- 最小值
- 自定义计算流程
- 是否动态计算,也就是运行时计算,还是一直使用添加时的数值
- 简单来说就是,是否锁面板
## Attribute
### 关于属性的思考
- 效果 A:是 `Duration` 类型,持续时间是 3s,`Period` 为 0,作用是 Add HP 50%
- 效果 B:是 `Instant` 类型,作用是 Add HP -5
先执行 A,再执行 B
```bash
Current Value = 40.0 Base Value = 40.0
Current Value = 60.0 Base Value = 40.0
Current Value = 52.5 Base Value = 35.0
Current Value = 35.0 Base Value = 35.0 // 3s 之后
```
很明显,由于 `Instant` 是直接修改 `BaseValue`,导致效果 A 基于 `BaseValue` 增加 50% 后计算得到值是 52.5
从玩家体验来看,理论上来说应该扣除 5 点血量,剩余 55 血量,结果实际上是 52.5。对用户来说这就是明显的 Bug
> 这里关于 `Instant` 属性修改,可以先看下面 **GE 的时间周期分类**
于是乎,我们想到一种解决方案
以血量为例,除开 `HP`、`MaxHP` 之外,再额外加上 `ExtraHP` 和 `Damage`
对血量的操作并不直接作用于 `HP`,而是作用于 `ExtraHP`
1. 假设初始血量为 40
2. 使用效果 A,增加血量 50%,此时 ExtraHP 值为 20
3. 使用效果 B,减少血量 5点,此时 Damage 值为 5
4. 计算得到 HP 的 `CurrentValue` 为 55,即 `HP + ExtraHP - Damage`
5. 效果 A 时间结束,结束时根据 `ExtraHP` 的值动态修改 `Damage` 值,此时 `ExtraHP` 为 0,`Damage` 值为 0(因为 20 > 5,如果 `Damage` 值为 30,那么效果 A 结束时 Damage 更新为 10)
6. 计算得到 HP 的 `CurrentValue` 为 `HP + ExtraHP - Damage` = 4
> 临时性 HP 修改全部作用于 ExtraHP,比如 效果A,或者其他 Buff、Debuff
> 直接扣除 HP 的全部作用于 Damage
> 增加当前 HP 的全部作用于 HP
## GE
### GE 的时间周期分类

GE 的时间配置如上图所示,分为三种类型
| 周期类型 | 作用 |
| --- | --- |
| Instant | 立即执行一次 |
| Infinite | 永久持续 |
| Has Duration | 持续一段时间 |
`Period` 用于配置执行周期
根据周期类型的不同,`Period` 数值的不同,会出现几种不同的情况
| Duration 配置 | 效果 | 解释 |
| --- | --- | --- |
| DurationPolicy: Instant |  | 修改 BaseValue(current也跟着变) |
| DurationPolicy: Infinite
Period: 0.0 |  | 只修改 CurrentValue,BaseValue 不变 |
| DurationPolicy: Infinite
Period: 1.0 |  | 修改 BaseValue(current也跟着变) |
| DurationPolicy: HasDuration
ScalableFloatMagnitude: 2.0
Period: 0.0 |  | 修改了CurrentValue,BaseValue不变,2s 后GE消除,CurrentValue 恢复到 100 |
| DurationPolicy: HasDuration
ScalableFloatMagnitude: 2.0
Period: 1.0 |  | 直接修改了 BaseValue (current也跟着变) |
分析来看,这是合理的
- 如果一个 `Effect` 是周期性的,但是 `Period` 为 0,表示这是一个增益性的 Buff,在持续时间结束后需要清除,所以修改 `CurrentValue` 更为合适
- 如果一个 `Effect` 是周期性的,并且 `Period` 不为 0 或者这个 GE 是 `Instant` 的,那么生效时应该修改 `BaseValue`
但是这是一个隐式的计算过程,对外并不可见,可能会导致使用者的使用错误,所以需要添加额外说明
通过查看源码,可以发现对属性的所有修改操作,会封装成一个 `Mod` 的数组,基于 `BaseValue` 和 `Mod` 数组,最终计算得到 `CurrentValue`
> `Mod` 是 `Modifier` 的缩写
### 关于 UGameplayEffect、FGameplayEffectSpec 和 FActiveGameplayEffect
`UGameplayEffect` 用于配置,新建的 GE 的资产就是这个
`FGameplayEffectSpec` 包含 `UGameplayEffect` 和 对应的运行时信息,比如运行时计算得到的 `Duration`、`Period` 等
`FActiveGameplayEffect` 包含 `UGameplayEffect`,当一个 GE 是周期性时,会加入到 `FActiveGameplayEffectsContainer` 容器中,需要记录 GE 执行的 开始时间、下一次执行时间 等信息
综上,不同的配置用于的不同的情况,但是无论如何,一个 GE 只要被应用了,就会创建对应的 `FGameplayEffectSpec` 实例
### GE 的添加流程
以 `BlueprintCallable` 的 `BP_ApplyGameplayEffectToSelf` 作为入口
1. 通过传入的 `TSubclassOf` 创建 GE 对象
2. 调用 `ApplyGameplayEffectToTarget`
```cpp
UGameplayEffect* GameplayEffect = GameplayEffectClass->GetDefaultObject();
return ApplyGameplayEffectToTarget(GameplayEffect, Target, Level, Context); 注意这里是通过 GetDefaultObject 获取目标的 CDO 对象,也就是说所有
```
GE 在这里得到的 `GameplayEffect` 都是同一个对象,这为后面判断堆叠提供了帮助
在 `ApplyGameplayEffectToTarget` 中
1. 通过传入参数创建 `FGameplayEffectSpec`
2. 调用 `ApplyGameplayEffectSpecToTarget`
```cpp
FGameplayEffectSpec Spec(GameplayEffect, Context, Level);
return ApplyGameplayEffectSpecToTarget(Spec, Target, PredictionKey);在 ApplyGameplayEffectSpecToTarget 中
```
1. 判断是否需要预测,以此来清空预测键 `PredictionKey`
2. 调用 `Target->ApplyGameplayEffectSpecToSelf` 获得 GE 的 `Handle`
接下来所有代码都不考虑网络同步和预测的问题
在 `Target->ApplyGameplayEffectSpecToSelf` 中
3. 检查
- 通过 `GameplayEffectApplicationQueries` 检查当前要添加的 GE 是否有效
- 通过 `ActiveGameplayEffects` 和当前要添加的 GE 检查能否添加
- 通过 `GEComponent` 来判断的
- 例如:`AssetTagsGameplayEffectComponent`、`AbilitiesGameplayEffectComponent` 等 `GEComponent` 配置
- 检查当前准备添加的 GE 是否配置了有效的 `Attribute`
4. 判断是否是立即执行的 GE
- 如果是立即执行的 GE:`ExecuteGameplayEffect(*OurCopyOfSpec, PredictionKey)` 直接执行 GE
- 如果是持续时间的 GE:`ActiveGameplayEffects.ApplyGameplayEffectSpec` 添加 GE 到容器中
在 `ActiveGameplayEffects.ApplyGameplayEffectSpec` 中
1. 判断堆叠
- 如果堆叠:更新叠层计数,根据条件刷新持续时间、重置计数器等操作,使用现有的 `FActiveGameplayEffect` 实例
- 如果不堆叠:创建 `FActiveGameplayEffect` 实例
2. 重新计算 `FActiveGameplayEffect` 数值,收集其绑定的属性
3. 计算持续时间和周期,绑定 Timer 到 `FActiveGameplayEffect` 实例上
4. 根据是否堆叠触发不同的事件
- 如果堆叠:触发 `OnStackCountChange`
- 如果不堆叠:触发 `InternalOnActiveGameplayEffectAdded`
在代码中有两个地方使用了 `TimeManager`
- 第一个 Timer 是在持续时间结束后触发,用于标记效果过期(可以移除效果),只触发一次所以 `bLoop` 设置为 false
- 第二个 Timer 是固定时间执行周期性效果(每秒造成1点伤害),需要循环触发所以设置为 true
- 如果 GE 的 `bExecutePeriodicEffectOnApplication` 为 true,表示 GE 添加时立刻执行一次,所以会额外注册一个 TimerManager.SetTimerForNextTick
```cpp
// Calculate Duration mods if we have a real duration
if (DurationBaseValue > 0.f)
{
float FinalDuration = AppliedEffectSpec.CalculateModifiedDuration();
// 一些判断和特殊数据处理
if (Owner && bSetDuration)
{
FTimerManager& TimerManager = Owner->GetWorld()->GetTimerManager();
FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::CheckDurationExpired, AppliedActiveGE->Handle);
TimerManager.SetTimer(AppliedActiveGE->DurationHandle, Delegate, FinalDuration, false);
if (!ensureMsgf(AppliedActiveGE->DurationHandle.IsValid(), TEXT("Invalid Duration Handle after attempting to set duration for GE %s @ %.2f"),
*AppliedActiveGE->GetDebugString(), FinalDuration))
{
// Force this off next frame
TimerManager.SetTimerForNextTick(Delegate);
}
}
}
// Register period callbacks with the timer manager
if (bSetPeriod && Owner && (AppliedEffectSpec.GetPeriod() > UGameplayEffect::NO_PERIOD))
{
FTimerManager& TimerManager = Owner->GetWorld()->GetTimerManager();
FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::ExecutePeriodicEffect, AppliedActiveGE->Handle);
// The timer manager moves things from the pending list to the active list after checking the active list on the first tick so we need to execute here
if (AppliedEffectSpec.Def->bExecutePeriodicEffectOnApplication)
{
TimerManager.SetTimerForNextTick(Delegate);
}
TimerManager.SetTimer(AppliedActiveGE->PeriodHandle, Delegate, AppliedEffectSpec.GetPeriod(), true);
}
```
在 `InternalOnActiveGameplayEffectAdded` 函数中
1. 会根据 GEComponent 来判断当前 GE 能否被激活或者说是否被抑制(`bIsInhibited`)
2. 调用 `UAbilitySystemComponent::InhibitActiveGameplayEffect`
在 `UAbilitySystemComponent::InhibitActiveGameplayEffect` 函数中
1. 根据 GE 是否被 抑制
- 被抑制:执行 `RemoveActiveGameplayEffectGrantedTagsAndModifiers`
- 不被抑制:执行 `AddActiveGameplayEffectGrantedTagsAndModifiers`
2. 触发 `OnInhibitionChanged` 事件
3. 在 `FScopedAggregatorOnDirtyBatch` 对象析构的时候触发
- `ActiveGameplayEffects.OnMagnitudeDependencyChange` 属性变化事件
- `OnDirty` 脏数据事件触发
#### 流程图


#### 代码执行过程

### GE 的执行流程
```cpp
FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::ExecutePeriodicEffect, AppliedActiveGE->Handle);
```
根据上面的代码,直接定位执行位置是 `UAbilitySystemComponent::ExecutePeriodicEffect`
通过函数调用,真正执行的代码在 `FActiveGameplayEffectsContainer::ExecuteActiveEffectsFrom` 函数中
1. 设置 `TargetTags` 即 `Owner` 标记上的 `GameplayTags`
2. 计算 `GE` 的 `Modifiers`
3. 通过循环调用 `InternalExecuteMod` 应用 `Modifier`
4. 遍历 `Executions` 数组,计算得到 `ConditionalEffectSpecs` 将要附加的 GE
5. 触发 GC
6. 应用 `ConditionalEffectSpecs`
7. 最后触发事件
重点在于两个函数 `CalculateModifierMagnitudes` 和 `InternalExecuteMod`
在 `CalculateModifierMagnitudes` 中
```cpp
const FGameplayModifierInfo& ModDef = Def->Modifiers[ModIdx];
FModifierSpec& ModSpec = Modifiers[ModIdx];
if (ModDef.ModifierMagnitude.AttemptCalculateMagnitude(*this, ModSpec.EvaluatedMagnitude) == false)
{
ModSpec.EvaluatedMagnitude = 0.f;
ABILITY_LOG(Warning, TEXT("Modifier on spec: %s was asked to CalculateMagnitude and failed, falling back to 0."), *ToSimpleString());
}
```
很清晰,通过在 GE 中配置的 `Def->Modifiers` 计算出 `ModSpec.EvaluatedMagnitude` 的值
```cpp
switch (MagnitudeCalculationType)
{
case EGameplayEffectMagnitudeCalculation::ScalableFloat:break;
case EGameplayEffectMagnitudeCalculation::AttributeBased:break;
case EGameplayEffectMagnitudeCalculation::CustomCalculationClass:break;
case EGameplayEffectMagnitudeCalculation::SetByCaller:break;
// ...
}
```
在 `AttemptCalculateMagnitude` 中通过枚举,来计算具体的值内容
在 `InternalExecuteMod` 函数中,核心代码如下
```cpp
if (AttributeSet->PreGameplayEffectExecute(ExecuteData))
{
float OldValueOfProperty = Owner->GetNumericAttribute(ModEvalData.Attribute);
ApplyModToAttribute(ModEvalData.Attribute, ModEvalData.ModifierOp, ModEvalData.Magnitude, &ExecuteData);
FGameplayEffectModifiedAttribute* ModifiedAttribute = Spec.GetModifiedAttribute(ModEvalData.Attribute);
if (!ModifiedAttribute)
{
// If we haven't already created a modified attribute holder, create it
ModifiedAttribute = Spec.AddModifiedAttribute(ModEvalData.Attribute);
}
ModifiedAttribute->TotalMagnitude += ModEvalData.Magnitude;
{
SCOPE_CYCLE_COUNTER(STAT_PostGameplayEffectExecute);
/** This should apply 'gamewide' rules. Such as clamping Health to MaxHealth or granting +3 health for every point of strength, etc */
AttributeSet->PostGameplayEffectExecute(ExecuteData);
}
}
```
基本流程也很简单
1. 调用 `PreGameplayEffectExecute` 判断能否触发
2. 添加对应属性修改 `ApplyModToAttribute`
3. 添加属性的修改记录
4. 设置累加值
5. 触发事件 `PostGameplayEffectExecute`


### UGameplayEffectCalculation
`UGameplayEffectCalculation` 与 `UGameplayModMagnitudeCalculation` 类似,都是可以自定义属性的计算过程,不同的是 `UGameplayEffectCalculation` 可以对多个属性进行操作
以 `Lyra` 项目中的 `ULyraDamageExecution` 为例
```cpp
Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput)
```
首先观察参数
- `ExecutionParams` 是 `const &`,表示作为输入
- `OutExecutionOutput` 是 `&`,表示作为输出
通过 `ExecutionParams` 可以得到 `FGameplayEffectSpec` 这个 GE 的运行时实例
```cpp
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluateParameters;
EvaluateParameters.SourceTags = SourceTags;
EvaluateParameters.TargetTags = TargetTags;
float BaseDamage = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BaseDamageDef, EvaluateParameters, BaseDamage);
```
收集 `Source` 和 `Target` 身上的 `Tag`,通过 `AttemptCalculateCapturedAttributeMagnitude` 将捕获的**属性** 以及 收集的信息 进行计算得到 `BaseDamage`
> 捕获的属性是 `ULyraCombatSet::GetBaseDamageAttribute()`
> 这一步进行计算,得到属性值
接下来就是进行**一系列**计算:距离衰减、友伤判定、护甲衰减等,得到最终的数值 `DamageDone`
最后将 `DamageDown` 应用到属性上
```cpp
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(ULyraHealthSet::GetDamageAttribute(), EGameplayModOp::Additive, DamageDone));
```
注意,最开始获取伤害是通过 `ULyraCombatSet::GetBaseDamageAttribute` 属性,最后应用伤害是在 `ULyraHealthSet::GetDamageAttribute`
这也是 `UGameplayEffectCalculation` 的优势点
### UGameplayEffectComponent
为了方便配置、提升可扩展性,将 GE 执行的各个流程封装成成 `UGameplayEffectComponent`,根据各自的需求实现各自的共嫩
从源码来看,`UGameplayEffectComponent` 内容非常简单
| 函数 | 作用 |
| --- | --- |
| CanGameplayEffectApply | 检查 GE 是否可以应用到目标,必须所有的 `GEComponent` 都返回 True ,该 GE 才可以被应用 |
| OnActiveGameplayEffectAdded | 带持续时间的效果激活时,如果返回 False,表示该 GE 不能激活 |
| OnGameplayEffectExecuted | 在瞬时效果或周期效果的执行时触发 |
| OnGameplayEffectApplied | 效果首次应用或堆叠时的通用入口 |
| OnGameplayEffectChanged | 编辑器中对 GameplayEffect 的修改保存后 |
> `OnGameplayEffectChanged` 通常执行在 Editor 模式下
通过拆分 GE 的生命周期,根据各自的需求封装 `GEComponent`
-------
以 `UAdditionalEffectsGameplayEffectComponent` 为例
`UAdditionalEffectsGameplayEffectComponent` 的作用是当 GE 被 `Applied` 的时候,添加新的 GE 到 `Target` 上
- 在 `OnActiveGameplayEffectAdded` 激活时绑定了 GE 的 Removed 事件,方便在 Removed 的时候删除本次添加的 GE
- 在 `OnGameplayEffectApplied` 通过 `ConditionalEffect` 将能够添加的 GE 添加到 Target 上
-------
以 `UAssetTagsGameplayEffectComponent` 为例
`UAssetTagsGameplayEffectComponent` 的作用是为 GE 添加 Tags
- 在 `OnGameplayEffectChanged` 的时候,将 `InheritableAssetTags` 存储的内容添加到 GE 的 `CachedAssetTags` 属性中
- 在 `PostEditChangeProperty` 的时候,同步更新 GE 中的 `CachedAssetTags` 属性
> `PostEditChangeProperty` 是 UObject 的函数,当资产更新的时候触发,这里就是修改 `AssetTag` 的时候触发
--------
还有一个 `UGameplayEffectUIData`
`UGameplayEffectUIData` 本身没有 任何属性,也没有重写 任何函数
它的作用就是作为一个标记类,所有继承 `UGameplayEffectUIData` 的类都是用于 UI 显示的**配置**
比如 `Lyra` 项目中 `UTopDownArenaPickupUIData`,在项目中可以通过 `GetGameplayEffectUIData` 获取对应的继承 `UGameplayEffectUIData` 的配置项

> 上述中,获取到对应的 `UGameplayEffectUIData` 后,获取配置中的特效、贴图等配置,进行后续操作
--------
由于 `UGameplayEffectComponent` 的存在,现在 GE 的配置更加方便、简单、易读