GameplayEffect是GAS框架修改Tag和属性的关键工具。了解其工作原理能帮助我们更好的理解各个配置的功能与依赖关系,并且方便我们对其进行扩展。
应用GE
Duration Policy为Instant
的GE会走ExecuteGameplayEffect
函数, 1
void FActiveGameplayEffectsContainer::ExecuteActiveEffectsFrom(FGameplayEffectSpec &Spec, FPredictionKey PredictionKey)
Instant
的GE则走, 1
FActiveGameplayEffect* FActiveGameplayEffectsContainer::ApplyGameplayEffectSpec(const FGameplayEffectSpec& Spec, FPredictionKey& InPredictionKey, bool& bFoundExistingStackableGE)
FActiveGameplayEffectsContainer
,专门用来存激活中的GE以及激活GE相关功能,就是用他来调用上面两个方法, 1
2UPROPERTY(Replicated)
FActiveGameplayEffectsContainer ActiveGameplayEffects;
ApplyGameplayEffectSpec
从接口上看,我们会先构造一个FGameplayEffectSpec
作为输入,然后激活的时候会再构造一个FActiveGameplayEffect
,并在存进GameplayEffects_Internal
这个TArray中, 1
AppliedActiveGE = new(GameplayEffects_Internal) FActiveGameplayEffect(NewHandle, Spec, GetWorldTime(), GetServerWorldTime(), InPredictionKey);
operator new
使得上面的写法可以每次都把元素加到数组最后面。 1
2
3
4
5
6template <typename T,typename AllocatorType> void* operator new( size_t Size, TArray<T,AllocatorType>& Array )
{
check(Size == sizeof(T));
const auto Index = Array.AddUninitialized();
return &Array[Index];
}
Modifier
对于ExecuteActiveEffectsFrom
来说,Modifier的调用栈如下,
ApplyGameplayEffectSpec则涉及到Aggregator,后面一节说明。
Aggregator
Aggregator就是聚合器的意思,当你的GE是一个Infinite
或者Has Duration
,且其Period=0
时,就会对同种属性的若干个Modifier进行一个聚合计算。(Has Duration && Period=0
的功能其实就等同于Infinite
,即无限期生效)
聚合计算即,先把所有加法算出一个结果Additive
,然后再把所有乘法算出一个结果Multiplicitive
,所有除法算出一个结果Division
,然后再按照公式计算: 1
((X + Additive) * Multiplicitive) / Division
1
Multiplicitive = 1 + (Mod1.Magnitude - 1) + (Mod2.Magnitude - 1) + ...
1+(0.2+0.4)=1.6
,而不是1.2*1.4=1.68
。
先把公式的源码列出来,后面会详细说说是如何运作的, 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
for (const FAggregatorMod& Mod : Mods[EGameplayModOp::Override])
{
if (Mod.Qualifies())
{
return Mod.EvaluatedMagnitude;
}
}
float Additive = SumMods(Mods[EGameplayModOp::Additive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Additive), Parameters);
float Multiplicitive = SumMods(Mods[EGameplayModOp::Multiplicitive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Multiplicitive), Parameters);
float Division = SumMods(Mods[EGameplayModOp::Division], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Division), Parameters);
return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
}
调用栈如下:
Period != 0
则从定时器中触发,并且不会使用Aggregator,
数据结构
FActiveGameplayEffectsContainer::AddActiveGameplayEffectGrantedTagsAndModifiers
会遍历GE所有Modifier。对于每一个Modifier,都通过FindOrCreateAttributeAggregator
会查询和创建Aggregator
并存入下面的TMap中,每种属性对应一个Aggregator, 1
2
3struct GAMEPLAYABILITIES_API FActiveGameplayEffectsContainer : public FFastArraySerializer
{
TMap<FGameplayAttribute, FAggregatorRef> AttributeAggregatorMap;1
2
3
4
5
6struct GAMEPLAYABILITIES_API FAggregator : public TSharedFromThis<FAggregator>
{
float BaseValue;
FAggregatorModChannelContainer ModChannels;
TArray<FActiveGameplayEffectHandle> Dependents;
int32 BroadcastingDirtyCount;FAggregatorModChannelContainer
是用于存储Channel的结果并进行计算的结构,Channel其实就是为了把这个属性的多个Modifier分开使用不同的计算公式进行计算。BaseValue
是用来存储所计算出来的属性的BaseValue。Dependents
是用来存储依赖这个属性的其他GE,若这个属性发生变化,则其他GE也要触发相应的更新,在计算与监听属性章节会讲到。BroadcastingDirtyCount
则是为了防止属性之间循环依赖的计数器,循环10次之后就会终止(这是保底措施,设计上不应该产生循环)。
FAggregatorModChannelContainer
套了FAggregatorModChannel
,里面有个FAggregatorMod
数组,表示这个Channel下包括的Modifiers, 1
2
3struct GAMEPLAYABILITIES_API FAggregatorModChannel
{
TArray<FAggregatorMod> Mods[EGameplayModOp::Max];EvaluatedMagnitude
就是这个Modifier计算所得。 1
2
3
4
5
6struct GAMEPLAYABILITIES_API FAggregatorMod
{
const FGameplayTagRequirements* SourceTagReqs;
const FGameplayTagRequirements* TargetTagReqs;
float EvaluatedMagnitude;
float StackCount;
计算与监听属性
前面提到的FindOrCreateAttributeAggregator
,创建完后就会调AddAggregatorMod
, 1
2
3
4
5
6
7for (int32 ModIdx = 0; ModIdx < Effect.Spec.Modifiers.Num(); ++ModIdx)
{
FAggregator* Aggregator = FindOrCreateAttributeAggregator(Effect.Spec.Def->Modifiers[ModIdx].Attribute).Get();
if (ensure(Aggregator))
{
Aggregator->AddAggregatorMod(EvaluatedMagnitude, ModInfo.ModifierOp, ModInfo.EvaluationChannelSettings.GetEvaluationChannel(), &ModInfo.SourceTags, &ModInfo.TargetTags, Effect.PredictionKey.WasLocallyGenerated(), Effect.Handle);
}EvaluatedMagnitude
中,调用了BroadcastOnDirty()
触发聚合计算。并且向其依赖发广播,使得这个聚合器中某个Modifer的数值发生了改变,对其整个GE乃至其他GE的最终数值都发生变化, 1
2
3
4
5
6void FAggregator::AddAggregatorMod(float EvaluatedMagnitude, TEnumAsByte<EGameplayModOp::Type> ModifierOp, EGameplayModEvaluationChannel ModifierChannel, const FGameplayTagRequirements* SourceTagReqs, const FGameplayTagRequirements* TargetTagReqs, bool IsPredicted, FActiveGameplayEffectHandle ActiveHandle)
{
FAggregatorModChannel& ModChannelToAddTo = ModChannels.FindOrAddModChannel(ModifierChannel);
ModChannelToAddTo.AddMod(EvaluatedMagnitude, ModifierOp, SourceTagReqs, TargetTagReqs, IsPredicted, ActiveHandle);
BroadcastOnDirty();
}AddAggregatorMod
是在循环里面调用的,意味着在这个GE的Mod还没遍历完的时候就已经开始发送广播事件了。
那么这个广播事件会触发哪些回调呢?在FindOrCreateAttributeAggregator
创建Aggregator之后会注册回调, 1
NewAttributeAggregator->OnDirty.AddUObject(Owner, &UAbilitySystemComponent::OnAttributeAggregatorDirty, Attribute, false);
1
2
3
4void FActiveGameplayEffectsContainer::OnAttributeAggregatorDirty(FAggregator* Aggregator, FGameplayAttribute Attribute, bool bFromRecursiveCall)
{
float NewValue = Aggregator->Evaluate(EvaluationParameters);
InternalUpdateNumericalAttribute(Attribute, NewValue, nullptr, bFromRecursiveCall);ModChannels::EvaluateWithBase
,也就是我们一开始的聚合公式的那个函数, 1
2
3
4float FAggregator::Evaluate(const FAggregatorEvaluateParameters& Parameters) const
{
return ModChannels.EvaluateWithBase(BaseValue, Parameters);
}InternalUpdateNumericalAttribute
里面就会对AttributeSet进行修改。
属性依赖更新
GESpec初始化函数会对本GE涉及的属性进行存储, 1
2
3void FGameplayEffectSpec::Initialize(const UGameplayEffect* InDef, const FGameplayEffectContextHandle& InEffectContext, float InLevel)
{
SetupAttributeCaptureDefinitions();1
2UPROPERTY(NotReplicated)
FGameplayEffectAttributeCaptureSpecContainer CapturedRelevantAttributes;
函数ApplyGameplayEffectSpec
中如下代码调用,到应用GE时,需要对该GE涉及的属性进行监听, 1
AppliedEffectSpec.CapturedRelevantAttributes.RegisterLinkedAggregatorCallbacks(AppliedActiveGE->Handle);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void FGameplayEffectAttributeCaptureSpecContainer::RegisterLinkedAggregatorCallbacks(FActiveGameplayEffectHandle Handle) const
{
for (const FGameplayEffectAttributeCaptureSpec& CaptureSpec : SourceAttributes)
{
CaptureSpec.RegisterLinkedAggregatorCallback(Handle);
}
}
void FGameplayEffectAttributeCaptureSpec::RegisterLinkedAggregatorCallback(FActiveGameplayEffectHandle Handle) const
{
if (BackingDefinition.bSnapshot == false)
{
FAggregator* Agg = AttributeAggregator.Get();
Agg->AddDependent(Handle);
}
}Snapshot
的话,就会调用加入到Aggregator的Dependents
属性中。虽然这里调用套了很多层,本质上就是为了把当前GE涉及的所有属性的Aggregator拿出来,然后把自己注册到属性依赖里。
注意,虽然它在FGameplayEffectAttributeCaptureSpec
里拿到Aggregator,但并不代表Aggregator的生命期属于FGameplayEffectAttributeCaptureSpec
,实际上从前面源码分析可知,Aggregator的生命期是属于FActiveGameplayEffectsContainer
的,所以不同GE的同一种属性拿到的Aggregator是同一个。
当导致属性产生变化时,会调用BroadcastOnDirty
。其中会先对自身属性进行聚合计算,然后遍历Dependents
,使依赖该属性的GE重新计算。
触发依赖重新计算时,会调到依赖属性的FAggregator::UpdateAggregatorMod
,他采用的方法是重新构建一个FAggregatorMod
。因其本身也会对其依赖产生影响,所以要继续调BroadcastOnDirty
。 1
2
3FAggregatorModChannel& ModChannel = ModChannels.FindOrAddModChannel(ModDef.EvaluationChannelSettings.GetEvaluationChannel());
ModChannel.AddMod(Spec.GetModifierMagnitude(ModIdx, true), ModDef.ModifierOp, &ModDef.SourceTags, &ModDef.TargetTags, bWasLocallyGenerated, InHandle);
BroadcastOnDirty();SetAttributeBaseValue
在更新AttributeSet的属性后还给Aggregator更新其BaseValue。比如玩家先上了一个Infinite的依赖生命值的GE,然后又上了一个Instance的修改生命值的GE,修改生命值后就会在该处修改Aggregator中生命值的BaseValue,然后SetBaseValue
中就会调用BroadcastOnDirty
。 1
2
3
4
5
6
7
8void FActiveGameplayEffectsContainer::SetAttributeBaseValue(FGameplayAttribute Attribute, float NewBaseValue)
{
FAggregatorRef* RefPtr = AttributeAggregatorMap.Find(Attribute);
if (RefPtr)
{
FAggregator* Aggregator = RefPtr->Get();
Aggregator->SetBaseValue(NewBaseValue);
}
自定义Channel
前面提到FAggregatorModChannelContainer
这个东西可以放多个Channel,而默认只会有一个Channel,想开多个可以在DefaultGame.ini
中加入如下配置, 1
2
3
4[/Script/GameplayAbilities.AbilitySystemGlobals]
bAllowGameplayModEvaluationChannels=true
GameplayModEvaluationChannelAliases[0]="Channel0"
GameplayModEvaluationChannelAliases[1]="Channel1"
我猜测它的设计用途是用来分开乘区进行属性计算的。如过不想这么做可以考虑不同乘区定义不同的属性,最后计算时再汇总,比如有时多个GE会对同一个乘区造成贡献。
预测
所谓预测就是客户端先执行,然后通知服务端,服务端检查,然后告诉客户端是否被认可。若没被认可则客户端回滚操作。
UE有一个通用的预测框架,写在GameplayPrediction.cpp
里。GA的预测就是用的他,如果你有自己需要预测的功能,也可以用他。
FPredictionKey
FPredictionKey
结构体内部属性没什么好讲的,你可以把他看作是一个Key的整体。使用默认构造函数出来的FPredictionKey
是被视为无效Key,必须是CreateNewPredictionKey
等方法构造出来的才是有效的。有效性可以通过IsValidKey
来判断。这么做其实是为了应对空值处理。
FPredictionKeyDelegates
FPredictionKeyDelegates是一个用来维护代理的结构体。 1
2
3
4
5
6
7
8
9struct FPredictionKeyDelegates
{
struct FDelegates
{
TArray<FPredictionKeyEvent> RejectedDelegates;
TArray<FPredictionKeyEvent> CaughtUpDelegates;
};
TMap<FPredictionKey::KeyType, FDelegates> DelegateMap;RejectedDelegates
表示被服务端拒绝时触发的代理。 CaughtUpDelegates
表示被服务端认可时触发的代理。 FPredictionKey
会作为一个索引从DelegateMap
中找到代理回调。
FPredictionKey里面有个封装函数可以注册代理回调, 1
2
3FPredictionKey::NewRejectOrCaughtUpDelegate(FPredictionKeyEvent Event)
FPredictionKey::NewCaughtUpDelegate()
FPredictionKey::NewRejectedDelegate()1
2InPredictionKey.NewRejectOrCaughtUpDelegate(FPredictionKeyEvent::CreateUObject(Owner, &UAbilitySystemComponent::RemoveActiveGameplayEffect_NoReturn, AppliedActiveGE->Handle, -1));
ScopedPredictionKey.NewCaughtUpDelegate().BindUObject(this, &UAbilitySystemComponent::OnClientActivateAbilityCaughtUp, Handle, ScopedPredictionKey.Current);1
2FPredictionKeyDelegates::BroadcastRejectedDelegate(PredictionKey);
FPredictionKeyDelegates::BroadcastCaughtUpDelegate(PredictionKey)PredictionKey
以及注册成功或失败时的回调,然后开始干事情,然后再把PredictionKey
发送给服务端。服务端检查这事是否合法,若合法就发RPC给客户端,让它调用BroadcastCaughtUpDelegate
,若非法则让它调用BroadcastRejectedDelegate
。
FScopedPredictionWindow
FScopedPredictionWindow
结构体则是对PredictionKey
生命期管理的包装,利用RAII特性干两件事情: 1. 服务端从客户端获取到PredictionKey
时,将其放入该包装,然后马上对这个预测进行处理。最后利用自动变量的析构函数,在函数结束时将PredictionKey
清理并发回客户端。确保不会在处理其他事情时误访问到这个PredictionKey
。 2. 客户端利用该包装构造PredictionKey
,然后马上将PredictionKey
发给服务端。利用自动变量的析构函数,在函数结束时将PredictionKey
清理。确保不会在干其他事情时误使用到这个PredictionKey
。
通过这种方式,我们可以确保PredictionKey
只能被使用一次。上面两个特性分别对应两个构造函数以及一个析构函数。
第一个构造函数,客户端把PredictionKey
传给服务端时,服务端调用。它会把InPredictionKey
存入AbilitySystemComponent->ScopedPredictionKey
。 1
2
3
4
5
6FScopedPredictionWindow::FScopedPredictionWindow(UAbilitySystemComponent* AbilitySystemComponent, FPredictionKey InPredictionKey, bool InSetReplicatedPredictionKey /*=true*/)
{
if (AbilitySystemComponent->IsNetSimulating() == false)
{
AbilitySystemComponent->ScopedPredictionKey = InPredictionKey;
}PredictionKey
。注意新生成不代表会覆盖原来的,它有一个依赖的概念。如果原来的Key还没清理,那么可以认为我们需要连续预测多件事情。可以让新Key与旧Key连接。比如先后生成了三个Key,依赖关系是X->Y->Z,Y预测失败会同时将Y、Z两个预测操作拒绝/回退。 1
2
3FScopedPredictionWindow::FScopedPredictionWindow(UAbilitySystemComponent* InAbilitySystemComponent, bool bCanGenerateNewKey)
{
InAbilitySystemComponent->ScopedPredictionKey.GenerateDependentPredictionKey();ASC->ScopedPredictionKey
发回客户端去(发回客户端这一步暂不清楚是否是多余的)。然后客户端和服务端都会还原或清理其ScopedPredictionKey
。 1
2
3
4FScopedPredictionWindow::~FScopedPredictionWindow()
{
OwnerPtr->ReplicatedPredictionKeyMap.ReplicatePredictionKey(OwnerPtr->ScopedPredictionKey);
OwnerPtr->ScopedPredictionKey = RestoreKey;
GA的预测
配置为LocalPredicted
的GA在激活的时候就会构造FScopedPredictionWindow
,构造函数第二个参数为True明确表示会创建PredictionKey
。然后传进了CallServerTryActivateAbility
发往服务端。然后继续在本地激活GA。 1
2
3
4
5
6
7bool UAbilitySystemComponent::InternalTryActivateAbility(FGameplayAbilitySpecHandle Handle, FPredictionKey InPredictionKey, UGameplayAbility** OutInstancedAbility, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData)
{
if (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)
{
FScopedPredictionWindow ScopedPredictionWindow(this, true);
ActivationInfo.SetPredicting(ScopedPredictionKey);
CallServerTryActivateAbility(Handle, Spec->InputPressed, ScopedPredictionKey);ClientActivateAbilityFailed
通知客户端执行被拒绝的回调。(成功则不需要触发,因为没有需要在成功时执行的回调) 1
2
3void UAbilitySystemComponent::ClientActivateAbilityFailed_Implementation(FGameplayAbilitySpecHandle Handle, int16 PredictionKey)
{
FPredictionKeyDelegates::BroadcastRejectedDelegate(PredictionKey);
GE的预测
如果你用GA里面的接口来执行Apply GE,会默认调用GetPredictionKeyForNewAction
接口来获取PredictionKey
。这个接口会获取ScopedPredictionKey
,但如果你的执行不在GA的ScopedPredictionWindow
的生命期内(比如异步了),它就是一个无效的Key。这种情况你必须Apply前手动构建ScopedPredictionWindow
。 1
2
3
4
5TArray<FActiveGameplayEffectHandle> UGameplayAbility::ApplyGameplayEffectSpecToTarget(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle, const FGameplayAbilityTargetDataHandle& TargetData) const
{
for (TSharedPtr<FGameplayAbilityTargetData> Data : TargetData.Data)
{
Data->ApplyGameplayEffectSpec(*SpecHandle.Data.Get(), ActorInfo->AbilitySystemComponent->GetPredictionKeyForNewAction())PredictionKey
的,如果你直接调用它的接口,则无法具备预测功能。如果非要调用,你可以模仿GA手动调用GetPredictionKeyForNewAction
传进去。
ASC组件中的HasNetworkAuthorityToApplyGameplayEffect
进行了检查,只有在Authority
与传入有效PredictionKey
才能执行。使用默认构造函数的PredictionKey
是无效的。 1
2
3
4bool UAbilitySystemComponent::HasNetworkAuthorityToApplyGameplayEffect(FPredictionKey PredictionKey) const
{
return (IsOwnerActorAuthoritative() || PredictionKey.IsValidForMorePrediction());
}MarkArrayDirty()
以便于客户端本地能够修改FActiveGameplayEffectsContainer
。如果是服务端则走if部分代码,MarkItemDirty
使得AppliedActiveGE
能够快速同步给客户端,这是FFastArraySerializer
的功能。 1
2
3
4
5
6
7
8
9
10
11FActiveGameplayEffect* FActiveGameplayEffectsContainer::ApplyGameplayEffectSpec(const FGameplayEffectSpec& Spec, FPredictionKey& InPredictionKey, bool& bFoundExistingStackableGE)
{
if (InPredictionKey.IsLocalClientKey() == false || IsNetAuthority())
{
MarkItemDirty(*AppliedActiveGE);
}
else
{
MarkArrayDirty();
InPredictionKey.NewRejectOrCaughtUpDelegate(FPredictionKeyEvent::CreateUObject(Owner, &UAbilitySystemComponent::RemoveActiveGameplayEffect_NoReturn, AppliedActiveGE->Handle, -1));
}ActiveGameplayEffectsContainer
,然后在客户端执行FActiveGameplayEffect::PostReplicatedAdd
,里面执行使GE生效的逻辑。
对于GE来说,它会无条件用服务端的Active GE去覆盖客户端本地生成的Active GE。严格的讲不能叫覆盖,而是客户端会执行服务端下发的Active GE,然后把自己预测的GE移除。上面注册的回调RemoveActiveGameplayEffect_NoReturn
就是干这个事情。 1
2
3
4void RemoveActiveGameplayEffect_NoReturn(FActiveGameplayEffectHandle Handle, int32 StacksToRemove=-1)
{
RemoveActiveGameplayEffect(Handle, StacksToRemove);
}
客户端属性修正
如果客户端存在预测GE,该GE可能会修改本地属性,如果该属性在服务端发生了变化,比如通过SetXXX()
来修改,而没有通过GE,那么该属性同步到客户端后并不会产生依赖计算。这种情况,我们应该通知客户端GE更新其Aggregator里面的BaseValue。
以Level
属性为例,我们需要把属性标记为可复制。然后在OnRep
函数中调用宏GAMEPLAYATTRIBUTE_REPNOTIFY
,这个宏就是用来更新Aggregator的。 1
2
3
4
5
6UPROPERTY(ReplicatedUsing=OnRep_Level, EditAnywhere, BlueprintReadWrite, Category=Attributes)
FGameplayAttributeData Level;
ATTRIBUTE_ACCESSORS(UWrapAttributeSet, Level);
UFUNCTION()
void OnRep_Level(const FGameplayAttributeData& OldValue);1
2
3
4
5
6
7
8
9
10void UWrapAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UWrapAttributeSet, Level, COND_None, REPNOTIFY_Always);
}
void UWrapAttributeSet::OnRep_Level(const FGameplayAttributeData& OldValue)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UWrapAttributeSet, Level, OldValue);
}SetBaseAttributeValueFromReplication
来更新属性, 1
2
3
4
5#define GAMEPLAYATTRIBUTE_REPNOTIFY(ClassName, PropertyName, OldValue) \
{ \
static FProperty* ThisProperty = FindFieldChecked<FProperty>(ClassName::StaticClass(), GET_MEMBER_NAME_CHECKED(ClassName, PropertyName)); \
GetOwningAbilitySystemComponentChecked()->SetBaseAttributeValueFromReplication(FGameplayAttribute(ThisProperty), PropertyName, OldValue); \
}
堆叠
在Apply是会检查一次目标GE是否已经存在并可以堆叠。函数位置如下, 1
FActiveGameplayEffect* FActiveGameplayEffectsContainer::FindStackableActiveGameplayEffect(const FGameplayEffectSpec& Spec)
StackingType
不为None,且DurationPolicy不为Instant的时候,从所有Active Effect里查找,CDO为同一对象的,并且发起者为同一人时则认为可以堆叠。
发现可堆叠后会检查一次是否达到堆叠上限StackLimitCount
,若到了上限,再检查是否有需要Apply的OverflowEffects
。以及若配置了DenyOverflowApplication
,则还会清理掉堆叠的GE。 1
2UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Overflow, meta=(EditCondition="bDenyOverflowApplication"))
bool bClearStackOnOverflow;
StackDurationRefreshPolicy
配置之后,若发生堆叠则更新该Active GE的开始时间之类的参数,以及重新计时。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18bool bSetDuration = true;
if (GEDef->StackDurationRefreshPolicy == EGameplayEffectStackingDurationPolicy::NeverRefresh)
{
bSetDuration = false;
}
else
{
RestartActiveGameplayEffectDuration(*ExistingStackableGE);
}
if (Owner && bSetDuration)
{
FTimerManager& TimerManager = Owner->GetWorld()->GetTimerManager();
FTimerDelegate Delegate = FTimerDelegate::CreateUObject(Owner, &UAbilitySystemComponent::CheckDurationExpired, AppliedActiveGE->Handle);
TimerManager.SetTimer(AppliedActiveGE->DurationHandle, Delegate, FinalDuration, false);
TimerManager.SetTimerForNextTick(Delegate);
}StackPeriodResetPolicy
与StackDurationRefreshPolicy
类似,就不赘述了。
StackExpirationPolicy
则定义GE的Duration到期后要如何处理。比如你中毒BUFF到期后是减一层呢还是整个BUFF去掉。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17switch(Effect.Spec.Def->StackExpirationPolicy)
{
case EGameplayEffectStackingExpirationPolicy::ClearEntireStack:
StacksToRemove = -1; // Remove all stacks
CheckForFinalPeriodicExec = true;
break;
case EGameplayEffectStackingExpirationPolicy::RemoveSingleStackAndRefreshDuration:
StacksToRemove = 1;
CheckForFinalPeriodicExec = (Effect.Spec.StackCount == 1);
RefreshStartTime = true;
RefreshDurationTimer = true;
break;
case EGameplayEffectStackingExpirationPolicy::RefreshDuration:
RefreshStartTime = true;
RefreshDurationTimer = true;
break;
};
自定义属性计算的自定义依赖
自定义属性计算就是继承UGameplayModMagnitudeCalculation
来实现自己的计算公式,本身流程有较多重复,这里就不赘述了。
它有个特别的功能,除了默认的属性依赖外,它可以自定义依赖。实现方式是定义一个代理,然后需要重新计算时,就触发这个代理。
AddCustomMagnitudeExternalDependencies
中有如下代码, 1
2
3
4
5
6FOnExternalGameplayModifierDependencyChange* ExternalDelegate = ModCalcClassCDO->GetExternalModifierDependencyMulticast(Effect.Spec, World);
if (ExternalDelegate && (bIsNetAuthority || ModCalcClassCDO->ShouldAllowNonNetAuthorityDependencyRegistration()))
{
FCustomModifierDependencyHandle& NewDependencyHandle = CustomMagnitudeClassDependencies.Add(ModCalcClassKey);
NewDependencyHandle.ActiveDelegateHandle = ExternalDelegate->AddRaw(this, &FActiveGameplayEffectsContainer::OnCustomMagnitudeExternalDependencyFired, ModCalcClass);
NewDependencyHandle.ActiveEffectHandles.Add(Effect.Handle);GetExternalModifierDependencyMulticast
返回指定代理即可。
GameplayEffect对CDO的修改
GE里面有个特性,你给GameplayEffectAssetTag里的Added增加一个Tag,编译之后CombinedTags配置也会自动新增这个Tag。这部分的代码位置如下, 1
void UGameplayEffect::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
PostEditChangeProperty
是一个基类函数,可以监听所有配置的变化。里面的实现就是获取了CDO然后,然后修改CDO里面的CombinedTags。
以后有类似修改一个配置就自动修改另一个配置的需求可以参考这个做法。但要注意一致性,有可能你自动修改后,又有人不小心改错了这个自动配置的参数。CombinedTags是一个只读参数所以不会有一致性问题。
Comments