Unreal GameplayEffect 源码分析

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)
ASC组件中有一个FActiveGameplayEffectsContainer,专门用来存激活中的GE以及激活GE相关功能,就是用他来调用上面两个方法,
1
2
UPROPERTY(Replicated)
FActiveGameplayEffectsContainer ActiveGameplayEffects;

ApplyGameplayEffectSpec

从接口上看,我们会先构造一个FGameplayEffectSpec作为输入,然后激活的时候会再构造一个FActiveGameplayEffect,并在存进GameplayEffects_Internal这个TArray中,

1
AppliedActiveGE = new(GameplayEffects_Internal) FActiveGameplayEffect(NewHandle, Spec, GetWorldTime(), GetServerWorldTime(), InPredictionKey);
TArray重载了operator new使得上面的写法可以每次都把元素加到数组最后面。
1
2
3
4
5
6
template <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然后加起来,
1
Multiplicitive = 1 + (Mod1.Magnitude - 1) + (Mod2.Magnitude - 1) + ...
比如你配了两个Mod,一个是乘1.2,另一个是乘1.4,那么结果就是乘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
15
float 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;
}
按照GASDocument里的说法是,这种做法是为了适应虚幻争霸的设计。RPG游戏里大部分情况下属性加成都是在同一乘区的。

调用栈如下:

Period != 0则从定时器中触发,并且不会使用Aggregator,

数据结构

FActiveGameplayEffectsContainer::AddActiveGameplayEffectGrantedTagsAndModifiers会遍历GE所有Modifier。对于每一个Modifier,都通过FindOrCreateAttributeAggregator会查询和创建Aggregator并存入下面的TMap中,每种属性对应一个Aggregator,

1
2
3
struct GAMEPLAYABILITIES_API FActiveGameplayEffectsContainer : public FFastArraySerializer
{
TMap<FGameplayAttribute, FAggregatorRef> AttributeAggregatorMap;
FAggregator结构中值得关注的是下面几个属性,
1
2
3
4
5
6
struct 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
3
struct GAMEPLAYABILITIES_API FAggregatorModChannel
{
TArray<FAggregatorMod> Mods[EGameplayModOp::Max];
其中EvaluatedMagnitude就是这个Modifier计算所得。
1
2
3
4
5
6
struct GAMEPLAYABILITIES_API FAggregatorMod
{
const FGameplayTagRequirements* SourceTagReqs;
const FGameplayTagRequirements* TargetTagReqs;
float EvaluatedMagnitude;
float StackCount;

计算与监听属性

前面提到的FindOrCreateAttributeAggregator,创建完后就会调AddAggregatorMod

1
2
3
4
5
6
7
for (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);
}
AddMod完之后就马上算出这个Mod的数值并存到EvaluatedMagnitude中,调用了BroadcastOnDirty()触发聚合计算。并且向其依赖发广播,使得这个聚合器中某个Modifer的数值发生了改变,对其整个GE乃至其他GE的最终数值都发生变化,
1
2
3
4
5
6
void 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
4
void FActiveGameplayEffectsContainer::OnAttributeAggregatorDirty(FAggregator* Aggregator, FGameplayAttribute Attribute, bool bFromRecursiveCall)
{
float NewValue = Aggregator->Evaluate(EvaluationParameters);
InternalUpdateNumericalAttribute(Attribute, NewValue, nullptr, bFromRecursiveCall);
然后就调到ModChannels::EvaluateWithBase,也就是我们一开始的聚合公式的那个函数,
1
2
3
4
float FAggregator::Evaluate(const FAggregatorEvaluateParameters& Parameters) const
{
return ModChannels.EvaluateWithBase(BaseValue, Parameters);
}
InternalUpdateNumericalAttribute里面就会对AttributeSet进行修改。

属性依赖更新

GESpec初始化函数会对本GE涉及的属性进行存储,

1
2
3
void FGameplayEffectSpec::Initialize(const UGameplayEffect* InDef, const FGameplayEffectContextHandle& InEffectContext, float InLevel)
{
SetupAttributeCaptureDefinitions();
最终存到这个属性里,
1
2
UPROPERTY(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
16
void 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
3
FAggregatorModChannel& 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
8
void 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"
Infinite或Has Duration的GE中Modifier就可以选择Channel了。

我猜测它的设计用途是用来分开乘区进行属性计算的。如过不想这么做可以考虑不同乘区定义不同的属性,最后计算时再汇总,比如有时多个GE会对同一个乘区造成贡献。

预测

所谓预测就是客户端先执行,然后通知服务端,服务端检查,然后告诉客户端是否被认可。若没被认可则客户端回滚操作。

UE有一个通用的预测框架,写在GameplayPrediction.cpp里。GA的预测就是用的他,如果你有自己需要预测的功能,也可以用他。

FPredictionKey

FPredictionKey结构体内部属性没什么好讲的,你可以把他看作是一个Key的整体。使用默认构造函数出来的FPredictionKey是被视为无效Key,必须是CreateNewPredictionKey等方法构造出来的才是有效的。有效性可以通过IsValidKey来判断。这么做其实是为了应对空值处理。

FPredictionKeyDelegates

FPredictionKeyDelegates是一个用来维护代理的结构体。

1
2
3
4
5
6
7
8
9
struct FPredictionKeyDelegates
{
struct FDelegates
{
TArray<FPredictionKeyEvent> RejectedDelegates;
TArray<FPredictionKeyEvent> CaughtUpDelegates;
};

TMap<FPredictionKey::KeyType, FDelegates> DelegateMap;
RejectedDelegates表示被服务端拒绝时触发的代理。 CaughtUpDelegates表示被服务端认可时触发的代理。 FPredictionKey会作为一个索引从DelegateMap中找到代理回调。

FPredictionKey里面有个封装函数可以注册代理回调,

1
2
3
FPredictionKey::NewRejectOrCaughtUpDelegate(FPredictionKeyEvent Event)
FPredictionKey::NewCaughtUpDelegate()
FPredictionKey::NewRejectedDelegate()
实际调用例子如下,
1
2
InPredictionKey.NewRejectOrCaughtUpDelegate(FPredictionKeyEvent::CreateUObject(Owner, &UAbilitySystemComponent::RemoveActiveGameplayEffect_NoReturn, AppliedActiveGE->Handle, -1));
ScopedPredictionKey.NewCaughtUpDelegate().BindUObject(this, &UAbilitySystemComponent::OnClientActivateAbilityCaughtUp, Handle, ScopedPredictionKey.Current);
注册回调有了,那么触发回调呢?下面两个静态方法分别对应拒绝和认可。
1
2
FPredictionKeyDelegates::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
6
FScopedPredictionWindow::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
3
FScopedPredictionWindow::FScopedPredictionWindow(UAbilitySystemComponent* InAbilitySystemComponent, bool bCanGenerateNewKey)
{
InAbilitySystemComponent->ScopedPredictionKey.GenerateDependentPredictionKey();
析构函数,如果是在服务端调用,就会把ASC->ScopedPredictionKey发回客户端去(发回客户端这一步暂不清楚是否是多余的)。然后客户端和服务端都会还原或清理其ScopedPredictionKey
1
2
3
4
FScopedPredictionWindow::~FScopedPredictionWindow()
{
OwnerPtr->ReplicatedPredictionKeyMap.ReplicatePredictionKey(OwnerPtr->ScopedPredictionKey);
OwnerPtr->ScopedPredictionKey = RestoreKey;

GA的预测

配置为LocalPredicted的GA在激活的时候就会构造FScopedPredictionWindow,构造函数第二个参数为True明确表示会创建PredictionKey。然后传进了CallServerTryActivateAbility发往服务端。然后继续在本地激活GA。

1
2
3
4
5
6
7
bool 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);
失败的时候就会通过RPCClientActivateAbilityFailed通知客户端执行被拒绝的回调。(成功则不需要触发,因为没有需要在成功时执行的回调)
1
2
3
void 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
5
TArray<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())
要注意ASC组件的Apply GE接口默认是不会传PredictionKey的,如果你直接调用它的接口,则无法具备预测功能。如果非要调用,你可以模仿GA手动调用GetPredictionKeyForNewAction传进去。

ASC组件中的HasNetworkAuthorityToApplyGameplayEffect进行了检查,只有在Authority与传入有效PredictionKey才能执行。使用默认构造函数的PredictionKey是无效的。

1
2
3
4
bool UAbilitySystemComponent::HasNetworkAuthorityToApplyGameplayEffect(FPredictionKey PredictionKey) const
{
return (IsOwnerActorAuthoritative() || PredictionKey.IsValidForMorePrediction());
}
Apply时,如果是客户端执行则会走下面else部分代码,注册好回调,手动调用MarkArrayDirty()以便于客户端本地能够修改FActiveGameplayEffectsContainer。如果是服务端则走if部分代码,MarkItemDirty使得AppliedActiveGE能够快速同步给客户端,这是FFastArraySerializer的功能。
1
2
3
4
5
6
7
8
9
10
11
FActiveGameplayEffect* 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));
}
AppliedActiveGE会被同步至客户端的ActiveGameplayEffectsContainer,然后在客户端执行FActiveGameplayEffect::PostReplicatedAdd,里面执行使GE生效的逻辑。

对于GE来说,它会无条件用服务端的Active GE去覆盖客户端本地生成的Active GE。严格的讲不能叫覆盖,而是客户端会执行服务端下发的Active GE,然后把自己预测的GE移除。上面注册的回调RemoveActiveGameplayEffect_NoReturn就是干这个事情。

1
2
3
4
void 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
6
UPROPERTY(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
10
void 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
2
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Overflow, meta=(EditCondition="bDenyOverflowApplication"))
bool bClearStackOnOverflow;
这个功能的场景就类似于上中毒BUFF,中毒到第5层就造成大量伤害,并清掉中毒BUFF。

StackDurationRefreshPolicy配置之后,若发生堆叠则更新该Active GE的开始时间之类的参数,以及重新计时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool 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);
}
StackPeriodResetPolicyStackDurationRefreshPolicy类似,就不赘述了。

StackExpirationPolicy则定义GE的Duration到期后要如何处理。比如你中毒BUFF到期后是减一层呢还是整个BUFF去掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch(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
6
FOnExternalGameplayModifierDependencyChange* 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是一个只读参数所以不会有一致性问题。

Unreal BehaviorTree 源码分析 Kubernetes Client-go 源码解析

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×