Browse Source

feat: 添加 ActiveGEContainer 的添加 GE 的逻辑

NiceTry12138 6 months ago
parent
commit
66728b98fe
1 changed files with 308 additions and 0 deletions
  1. 308 0
      UE5/GAS/README.md

+ 308 - 0
UE5/GAS/README.md

@@ -615,3 +615,311 @@ OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(ULyraHealthS
 --------
 --------
 
 
 由于 `UGameplayEffectComponent` 的存在,现在 GE 的配置更加方便、简单、易读
 由于 `UGameplayEffectComponent` 的存在,现在 GE 的配置更加方便、简单、易读
+
+## FActiveGameplayEffectsContainer
+
+这是什么?
+
+这是一个存储激活的 GE (`FActiveGameplayEffect`) 的**容器**(`Container`)
+
+当一个 GE 被实例化的时候会封装成 `FGameplayEffectSpec`,当 GE 有持续时间时会通过 `ApplyGameplayEffectSpec` 添加到容器中,并封装成 `FActiveGameplayEffect`
+
+```cpp
+struct GAMEPLAYABILITIES_API FActiveGameplayEffectsContainer : public FFastArraySerializer
+```
+
+它继承自 `FFastArraySerializer`,是一种专为复制大型数组而设计的序列化方式
+
+- 适用于 `TArray` 类型。
+- 高效处理数组内任意位置的元素删除与新增,复制时仅发送变化的内容(Delta 序列化)。
+- 支持在客户端触发新增、删除事件回调
+
+> `MarkItemDirty` 对修改的 Entity 标脏
+
+`FActiveGameplayEffectsContainer` 维护一个数组 `GameplayEffects_Internal` 用于存储 Active GE 的实体
+
+```cpp
+UPROPERTY()
+TArray<FActiveGameplayEffect>	GameplayEffects_Internal;
+```
+
+| 类型 | 属性 | 作用 |
+| --- | --- | --- |
+| TMap<FGameplayAttribute, FAggregatorRef> | AttributeAggregatorMap | 每个被任何 Active GE 修改过的 FGameplayAttribute 维护一个 FAggregator |
+| TMap<FGameplayAttribute, FOnGameplayAttributeChange> | AttributeChangeDelegates | 注册当某个 FGameplayAttribute 的值发生变化时触发的回调委托 |
+| TMap<FGameplayAttribute, FOnGameplayAttributeValueChange> | AttributeValueChangeDelegates | 为每个 FGameplayAttribute 维护一个当该属性值发生有意义的变化(超出容差范围)时触发的回调委托 |
+| TMap<FObjectKey, FCustomModifierDependencyHandle> | CustomMagnitudeClassDependencies | 管理动态数值计算器 (UGameplayModMagnitudeCalculation 类) 的依赖关系 |
+| TMap<TWeakObjectPtr<UGameplayEffect>, TArray<FActiveGameplayEffectHandle> > | SourceStackingMap | 用于管理当这个 ASC 是 GE 的 Instigator (来源) 时,该 GE 在其他目标身上的堆叠情况 |
+| mutable int32 | ScopedLockCount | 作用域锁 (Scoped Lock) 的计数器 |
+| int32 | PendingRemoves | 一个简单的计数器,记录当前有多少个 FActiveGameplayEffect 被标记为待移除 |
+
+### HandleActiveGameplayEffectStackOverflow
+
+处理堆叠逻辑
+
+将堆叠超过 `Limit` 时配置的 `OverflowEffects` 应用到 `Owner` 上
+
+```cpp
+for (TSubclassOf<UGameplayEffect> OverflowEffect : StackedGE->OverflowEffects)
+{
+  if (const UGameplayEffect* CDO = OverflowEffect.GetDefaultObject())
+  {
+    FGameplayEffectSpec NewGESpec;
+    NewGESpec.InitializeFromLinkedSpec(CDO, OverflowingSpec);
+    Owner->ApplyGameplayEffectSpecToSelf(NewGESpec);
+  }
+}
+```
+
+根据 `bClearStackOnOverflow` 配置,清理掉当前的 GE
+
+```cpp
+if (!bAllowOverflowApplication && StackedGE->bClearStackOnOverflow)
+{
+  Owner->RemoveActiveGameplayEffect(ActiveStackableGE.Handle);
+}
+```
+
+### ApplyGameplayEffectSpec
+
+首先找到 `Stack` 堆叠的 `ActiveGE`
+
+如果存在堆叠的 GE 则触发堆叠逻辑,否则创建新的 `ActiveGE`
+
+#### 处理堆叠逻辑
+
+- 如果 `ExistingStackableGE` 不为空,则表示找到能够堆叠的 GE
+
+如果当前 `ExistingStackableGE` 的堆叠层数已经等于 `StackLimitCount`,那么新添加的 GE 肯定会导致堆叠溢出
+
+需要执行 `HandleActiveGameplayEffectStackOverflow` 来处理堆叠溢出逻辑
+
+```cpp
+if (ExistingSpec.GetStackCount() == ExistingSpec.Def->StackLimitCount)
+{
+  if (!HandleActiveGameplayEffectStackOverflow(*ExistingStackableGE, ExistingSpec, Spec))
+  {
+    return nullptr;
+  }
+}
+```
+
+接下来处理堆叠层数,让 `ExistingStackableGE` 的堆叠层数是
+
+```cpp
+NewStackCount = ExistingSpec.GetStackCount() + Spec.GetStackCount();
+if (ExistingSpec.Def->StackLimitCount > 0)
+{
+  NewStackCount = FMath::Min(NewStackCount, ExistingSpec.Def->StackLimitCount);
+}
+
+// ... do something else
+
+ExistingStackableGE->Spec.SetStackCount(NewStackCount);
+```
+
+> 注意这里通过 `FMath::Min` 限制了堆叠层数不超过 `StackLimitCount`
+
+最后根据配置来处理时间周期相关的逻辑,比如重新计时等
+
+```cpp
+// Make sure the GE actually wants to refresh its duration
+if (GEDef->StackDurationRefreshPolicy == EGameplayEffectStackingDurationPolicy::NeverRefresh)
+{
+  bSetDuration = false;
+}
+else
+{
+  RestartActiveGameplayEffectDuration(*ExistingStackableGE);
+}
+```
+
+根据上面的代码,不难发现一个问题,那就是对堆叠层数的处理
+
+试想一种情况,当前有 4 层,新添加的 GE 是 2 层,`StackLimitCount` 的限制是 5 层
+
+根据上面的代码逻辑
+
+1. 由于当前层数 4 层不等于 `StackLimitCount`,所以不处理堆叠溢出逻辑
+2. 计算 `NewStackCount` 堆叠层数,由于 `FMath::Min` 所以最后 `NewStackCount` 等于 5
+3. 后续其他逻辑
+
+这里对**堆叠层数**的处理和**堆叠层数溢出**的处理有点违反习惯
+
+通常来说 `4 + 2 = 6 > 5` 应该触发堆叠溢出,但是没有,再下一次添加的时候才会触发,这里的逻辑需要特别记住
+
+#### 不堆叠 创建新的 ActiveGE
+
+- 如果 `ExistingStackableGE` 为空,则需要创建新的 `ActiveGE`
+
+这里需要关注一个成员属性 `ScopeLockCount`
+
+```cpp
+mutable int32 ScopedLockCount;
+```
+
+`ScopedLockCount` 用于表示当前的这个**容器**(`Container`)是否正在被使用,如果当前容器正在被使用
+
+`GameplayEffects_Internal.GetSlack()` 可以得到数组预分配内存中可容纳的额外元素数量。如果该值 `<= 0` 表示数组已满(或接近满),此时添加新元素会触发**内存重新分配**
+
+如果当前容器正在被使用,并且容器已满,那么就不能直接添加到容器中,需要添加到 `PendingGameplayEffectHead` 链表中
+
+```cpp
+if (ScopedLockCount > 0 && GameplayEffects_Internal.GetSlack() <= 0)
+```
+
+如果 `PendingGameplayEffectNext` 为空,则在堆上创建新的对象;如果不为空,则直接在现有内存上构建对象
+
+```cpp
+if (*PendingGameplayEffectNext == nullptr)
+{
+  // 堆分配新对象
+  AppliedActiveGE = new FActiveGameplayEffect(NewHandle, Spec, GetWorldTime(), GetServerWorldTime(), InPredictionKey);
+  *PendingGameplayEffectNext = AppliedActiveGE;
+}
+else
+{
+  // 重用内存
+  **PendingGameplayEffectNext = FActiveGameplayEffect(NewHandle, Spec, GetWorldTime(), GetServerWorldTime(), InPredictionKey);
+  AppliedActiveGE = *PendingGameplayEffectNext;
+}
+// 更新链表指针
+PendingGameplayEffectNext = &AppliedActiveGE->PendingNext;
+```
+
+如果可以直接添加到 `GameplayEffects_Internal` 数组中,则调用下面这个代码逻辑
+
+```cpp
+AppliedActiveGE = new(GameplayEffects_Internal) FActiveGameplayEffect(NewHandle, Spec, GetWorldTime(), GetServerWorldTime(), InPredictionKey);
+```
+
+上述代码使用了 `placement new`,关于其解释在[另一篇文章](../../cpp/内存机制.md#placement-new)中 
+
+简单来说 `placement new` 用于在一块已经创建了内存的地方调用构造函数
+
+通过上面这些条件判断,最终创建出 `ActiveGE` 并添加到合适的地方
+
+`ScopedLockCount` 用于表示当前的 `Container` 是否正在被使用,使用 `FScopedActiveGameplayEffectLock` 的构造和析构函数来 **增加** 或 **减少** `ScopedLockCount` 的值
+
+> `mutable` 关键字表示该变量可以在 `count` 函数中修改 
+
+```cpp
+void FActiveGameplayEffectsContainer::IncrementLock()
+{
+	ScopedLockCount++;
+}
+
+void FActiveGameplayEffectsContainer::DecrementLock()
+{
+	if (--ScopedLockCount == 0)
+  {
+    // 处理 PendingGameplayEffectHead 逻辑
+  }
+}
+
+FScopedActiveGameplayEffectLock::FScopedActiveGameplayEffectLock(FActiveGameplayEffectsContainer& InContainer)
+	: Container(InContainer)
+{
+	Container.IncrementLock();
+}
+
+FScopedActiveGameplayEffectLock::~FScopedActiveGameplayEffectLock()
+{
+	Container.DecrementLock();
+}
+```
+
+当 `FScopedActiveGameplayEffectLock` 析构的时候,去判断 `ScopedLockCount` 是否归零,如果归零了就可以去处理 `PendingGameplayEffectHead` 相关逻辑,将其真正添加到 `GameplayEffects_Internal` 数组中
+
+#### 后续处理
+
+1. 收集和初始化数据,绑定关联关系
+
+```cpp
+AppliedEffectSpec.CaptureAttributeDataFromTarget(Owner);
+AppliedEffectSpec.CalculateModifierMagnitudes();
+
+AppliedEffectSpec.CapturedRelevantAttributes.RegisterLinkedAggregatorCallbacks(AppliedActiveGE->Handle);
+```
+
+2. 计算持续时间,并绑定 `Timer`,在持续时间结束后调用 `UAbilitySystemComponent::CheckDurationExpired`
+
+```cpp
+float FinalDuration = AppliedEffectSpec.CalculateModifiedDuration();
+
+AppliedEffectSpec.SetDuration(FinalDuration, true);
+
+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))
+  {
+    TimerManager.SetTimerForNextTick(Delegate);
+  }
+}
+```
+
+3. 计算执行周期,并绑定 `Timer`,在执行时调用 `UAbilitySystemComponent::ExecutePeriodicEffect`
+
+```cpp
+FTimerManager& TimerManager = Owner->GetWorld()->GetTimerManager();
+FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::ExecutePeriodicEffect, AppliedActiveGE->Handle);
+
+if (AppliedEffectSpec.Def->bExecutePeriodicEffectOnApplication)
+{
+  TimerManager.SetTimerForNextTick(Delegate);
+}
+
+TimerManager.SetTimer(AppliedActiveGE->PeriodHandle, Delegate, AppliedEffectSpec.GetPeriod(), true);
+```
+
+4. 标记脏数据
+5. 后续处理,并抛出事件
+
+```cpp
+if (ExistingStackableGE)
+{
+  OnStackCountChange(*ExistingStackableGE, StartingStackCount, NewStackCount);
+}
+else
+{
+  InternalOnActiveGameplayEffectAdded(*AppliedActiveGE);
+}
+```
+
+### FActiveGameplayEffectHandle
+
+`FActiveGameplayEffectHandle` 用于表示 `FActiveGameplayEffect` 唯一 ID
+
+```cpp
+FActiveGameplayEffectHandle NewHandle = FActiveGameplayEffectHandle::GenerateNewHandle(Owner);
+```
+
+在 `FActiveGameplayEffect` 构造函数中存储在 `Handle` 属性中
+
+`Handle` 的创建逻辑也很简单
+
+```cpp
+static int32 GHandleID=0;
+FActiveGameplayEffectHandle NewHandle(GHandleID++);
+
+TWeakObjectPtr<UAbilitySystemComponent> WeakPtr(OwningComponent);
+
+GlobalActiveGameplayEffectHandles::Map.Add(NewHandle, WeakPtr);
+
+return NewHandle;
+```
+
+根据上面的代码,不难发现,就是定义了一个全局变量 `GHandleID`,每次都创建的时候都直接 ++ 即可
+
+主要是在全局作用域中存储 `Handle` 和 `Owner` 的关系
+
+```cpp
+namespace GlobalActiveGameplayEffectHandles
+{
+	static TMap<FActiveGameplayEffectHandle, TWeakObjectPtr<UAbilitySystemComponent>>	Map;
+}
+```