Unreal-GAS 源码解析:同步、预测

GameplayAbility

赋予GA

UGameplayAbility* Ability

AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(Ability, 0, INDEX_NONE, this))

// ActivatableAbilities 所有可激活的能力数组
UPROPERTY(ReplicatedUsing=OnRep_ActivateAbilities, BlueprintReadOnly, Category = "Abilities")
FGameplayAbilitySpecContainer ActivatableAbilities;

// This property will only send to the replay connection, or to the actors owner
Params.Condition = COND_ReplayOrOwner;
DOREPLIFETIME_WITH_PARAMS_FAST(UAbilitySystemComponent, ActivatableAbilities, Params);
FGameplayAbilitySpecHandle UAbilitySystemComponent::GiveAbility(const FGameplayAbilitySpec& Spec)
	FGameplayAbilitySpec& OwnedSpec = ActivatableAbilities.Items[ActivatableAbilities.Items.Add(Spec)];
	if (OwnedSpec.Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor)
		CreateNewInstanceOfAbility(OwnedSpec, Spec.Ability);
	// 标记为Dirty,使Spec复制到客户端
	MarkAbilitySpecDirty(OwnedSpec, true);

GA实例化策略

namespace EGameplayAbilityInstancingPolicy
	enum Type
		// 使用CDO执行业务逻辑,无法存在状态信息
		NonInstanced,
		// 每个Actor实例化一个,注意多次触发之间的延时行为
		InstancedPerActor,
		// 每次执行都创建实例
		InstancedPerExecution

创建GASpec

FGameplayAbilitySpec::FGameplayAbilitySpec(UGameplayAbility* InAbility, int32 InLevel, int32 InInputID, UObject* InSourceObject)
	: Ability(InAbility)
	, Level(InLevel)
	, InputID(InInputID)
	, SourceObject(InSourceObject)
	...
		// 创建时获取一个Handle
		Handle.GenerateNewHandle();
void FGameplayAbilitySpecHandle::GenerateNewHandle()
	static int32 GHandle = 1;
	Handle = GHandle++;

创建GA实例

UGameplayAbility* UAbilitySystemComponent::CreateNewInstanceOfAbility(FGameplayAbilitySpec& Spec, UGameplayAbility* Ability)
	// 使用GA CDO创建GA Instance
	if (Ability->HasAllFlags(RF_ClassDefaultObject))
		AbilityInstance = NewObject<UGameplayAbility>(Owner, Ability->GetClass());
	else
		AbilityInstance = NewObject<UGameplayAbility>(Owner, Ability->GetClass(), NAME_None, RF_NoFlags, Ability);
	// 存入GA Instance数组
	if (AbilityInstance->GetReplicationPolicy() != EGameplayAbilityReplicationPolicy::ReplicateNo)
		Spec.ReplicatedInstances.Add(AbilityInstance);
		AllReplicatedInstancedAbilities.Add(AbilityInstance);
	else
		Spec.NonReplicatedInstances.Add(AbilityInstance);

激活GA

通过Handle激活GA

bool UAbilitySystemComponent::TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation)
	FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(AbilityToActivate);
	// GA CDO
	UGameplayAbility* Ability = Spec->Ability;
	// 跳过模拟端
	if (NetMode == ROLE_SimulatedProxy)
		return false;
	// Local Only Filter: ClientTryActivateAbility
	...
	// Server Only Filter: CallServerTryActivateAbility
	...
	return InternalTryActivateAbility(AbilityToActivate);

当然,有其它上层的激活方法:

  • 通过Tag激活GA
bool UAbilitySystemComponent::TryActivateAbilitiesByTag(const FGameplayTagContainer& GameplayTagContainer, bool bAllowRemoteActivation)
	TArray<FGameplayAbilitySpec*> AbilitiesToActivate;
	// 找到所有可激活的匹配Tag的GA
	GetActivatableGameplayAbilitySpecsByAllMatchingTags(GameplayTagContainer, AbilitiesToActivate);
		for (const FGameplayAbilitySpec& Spec : ActivatableAbilities.Items)
			// 匹配Tag
			if (Spec.Ability && Spec.Ability->AbilityTags.HasAll(GameplayTagContainer))
				// 满足其它限制条件
				if (!bOnlyAbilitiesThatSatisfyTagRequirements || Spec.Ability->DoesAbilitySatisfyTagRequirements(*this))
					MatchingGameplayAbilities.Add(const_cast<FGameplayAbilitySpec*>(&Spec));
	bSuccess |= TryActivateAbility(GameplayAbilitySpec->Handle, bAllowRemoteActivation);

是否有Tag的Miss、Block

bool UGameplayAbility::DoesAbilitySatisfyTagRequirements(const UAbilitySystemComponent& AbilitySystemComponent, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const
	bool bBlocked = false;
	bool bMissing = false;
  • 通过GA类激活GA
bool TryActivateAbilityByClass(TSubclassOf<UGameplayAbility> InAbilityToActivate, bool bAllowRemoteActivation = true)
	// 判断CDO是否相同
	if (Spec.Ability == InAbilityCDO)
  • 通过Event激活GA
bool TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle AbilityToTrigger, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag Tag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component);
	if (Ability->ShouldAbilityRespondToEvent(ActorInfo, &TempEventData))

底层的激活函数都是InternalTryActivateAbility:

bool UAbilitySystemComponent::InternalTryActivateAbility(FGameplayAbilitySpecHandle Handle, FPredictionKey InPredictionKey, UGameplayAbility** OutInstancedAbility, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData)
	FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
	// 跳过模拟端
	if (NetRole == ROLE_SimulatedProxy)
		return false;
	UGameplayAbility* const CanActivateAbilitySource = InstancedAbility ? InstancedAbility : Ability;
	// 过程中维护InternalTryActivateAbilityFailureTags,讲失败原因通过Tag进行记录
	if (!CanActivateAbilitySource->CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, &InternalTryActivateAbilityFailureTags))
		NotifyAbilityFailed(Handle, CanActivateAbilitySource, InternalTryActivateAbilityFailureTags);
		return false;
	// InstancedPerActor下禁止重复激活
	if (Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor)
		if (Spec->IsActive())
			...
	Spec->ActiveCount++;
	// 见下
	if (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalOnly || (NetRole == ROLE_Authority))
		// PredictionKey
		// ClientActivateAbilitySucceed
		// MulticastActivateAbilitySucceed
		// CallActivateAbility
	else if (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)
		// PredictionKey
		// CallServerTryActivateAbility
		// CallActivateAbility

是否可以激活

bool UGameplayAbility::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const
	!ShouldActivateAbility(AvatarActor->GetLocalRole())
	// 检测是否有Cooldown Tag
	!CheckCooldown(Handle, ActorInfo, OptionalRelevantTags)
	// 通过CanApplyAttributeModifiers判断
	!CheckCost(Handle, ActorInfo, OptionalRelevantTags)
	!DoesAbilitySatisfyTagRequirements(*AbilitySystemComponent, SourceTags, TargetTags, OptionalRelevantTags)
	// Input阻塞
	AbilitySystemComponent->IsAbilityInputBlocked(Spec->InputID)
	// 蓝图自定义
	!K2_CanActivateAbility(*ActorInfo, Handle, OutTags)

最后的执行部分

void UGameplayAbility::CallActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData)
	PreActivate(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate);
	ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
// 自己去重载 Do something
void UGameplayAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
	// 可拆为CommitCost()、CommitCooldown()
	// CommitCheck、CommitExecute
	if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
		EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
	...

可以自己设置Timer或者回调调用EndAbility或者CancelAbility

EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
void UGameplayAbility::CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility)

在示例中,ActivateAbility创建GameplayTask进行非瞬时操作,通过绑定OnCompleted、OnCancelled、EventReceived进行GA流程控制

激活GA - 预测

执行策略:

namespace EGameplayAbilityNetExecutionPolicy
	enum Type
		// Client上传并本地预测
		LocalPredicted		UMETA(DisplayName = "Local Predicted"),
		// Client不上传
		LocalOnly			UMETA(DisplayName = "Local Only"),
		// Client上传,等Server
		ServerInitiated		UMETA(DisplayName = "Server Initiated"),
		// Server不下发
		ServerOnly			UMETA(DisplayName = "Server Only"),

通过PKey判断需要取消哪些GA

UAbilitySystemComponent::InternalTryActivateAbility
	// ROLE_Authority && (!InPredictionKey || ServerInitiated || ServerOnly)
	// Server新建PKey
	if (bCreateNewServerKey)	
		ActivationInfo.ServerSetActivationPredictionKey(FPredictionKey::CreateNewServerInitiatedKey(this));
	// 使用传入PKey
	else if (InPredictionKey.IsValidKey())
		ActivationInfo.ServerSetActivationPredictionKey(InPredictionKey);
	// NetExecutionPolicy: LocalPredicted
	// 见下
	FScopedPredictionWindow ScopedPredictionWindow(this, ActivationInfo.GetActivationPredictionKey());
	CallServerTryActivateAbility(Handle, Spec->InputPressed, ScopedPredictionKey);
		// RPC Server
		ServerTryActivateAbility(AbilityHandle, InputPressed, PredictionKey);
			// Server
			InternalServerTryActivateAbility(Handle, InputPressed, PredictionKey, nullptr);
				// 找到Server上的该技能(Handle的作用)
				FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
				FScopedPredictionWindow ScopedPredictionWindow(this, PredictionKey);
				// Server激活GA
				InternalTryActivateAbility(Handle, PredictionKey, &InstancedAbility, nullptr, TriggerEventData)
				// 如果过程中失败,会回调通知Client
				ClientActivateAbilityFailed(Handle, PredictionKey.Current);

Server失败的RPC通知

void UAbilitySystemComponent::ClientActivateAbilityFailed_Implementation(FGameplayAbilitySpecHandle Handle, int16 PredictionKey)
	// 广播预测键Rejected(作用参考见GE预测)
	FPredictionKeyDelegates::BroadcastRejectedDelegate(PredictionKey);
	
	FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
	if (Spec->ActivationInfo.GetActivationPredictionKey().Current == PredictionKey)
		Spec->ActivationInfo.SetActivationRejected();
			ActivationMode = EGameplayAbilityActivationMode::Rejected;
	// 取消所有相同PKey的GA
	TArray<UGameplayAbility*> Instances = Spec->GetAbilityInstances();
	for (UGameplayAbility* Ability : Instances)
		if (Ability->CurrentActivationInfo.GetActivationPredictionKey().Current == PredictionKey)
			Ability->K2_EndAbility();
				EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);

结束技能

void UGameplayAbility::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
	// 移除timers、latent actions
	MyWorld->GetLatentActionManager().RemoveActionsForObject(this);
	if (FAbilitySystemTweaks::ClearAbilityTimers)
		MyWorld->GetTimerManager().ClearAllTimersForObject(this);
	Task->TaskOwnerEnded();
	// 通知远端
	AbilitySystemComponent->ReplicateEndOrCancelAbility(Handle, ActivationInfo, this, false);
	// 移除Tag
	AbilitySystemComponent->RemoveLooseGameplayTags(ActivationOwnedTags);
	// 移除Cue
	AbilitySystemComponent->RemoveGameplayCue(GameplayCueTag);
	AbilitySystemComponent->HandleChangeAbilityCanBeCanceled(AbilityTags, this, false);
	AbilitySystemComponent->ApplyAbilityBlockAndCancelTags(AbilityTags, this, false, BlockAbilitiesWithTag, false, CancelAbilitiesWithTag);

GameplayEffect

施加GE

FActiveGameplayEffectHandle UGameplayAbility::ApplyGameplayEffectSpecToOwner(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle) const
	if (SpecHandle.IsValid() && (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo)))
		return AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get(), AbilitySystemComponent->GetPredictionKeyForNewAction());

bool UGameplayAbility::HasAuthorityOrPredictionKey(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo* ActivationInfo) const
	bool UAbilitySystemComponent::HasAuthorityOrPredictionKey(const FGameplayAbilityActivationInfo* ActivationInfo) const
		bool UAbilitySystemComponent::CanPredict() const
			bool IsValidForMorePrediction() const
				return Current > 0 && bIsStale == false && bIsServerInitiated == false;

FPredictionKey GetPredictionKeyForNewAction() const
	return ScopedPredictionKey.IsValidForMorePrediction() ? ScopedPredictionKey : FPredictionKey();
FActiveGameplayEffectHandle UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(const FGameplayEffectSpec &Spec, FPredictionKey PredictionKey)
	// 不预测周期GE
	if(PredictionKey.IsValidKey() && Spec.GetPeriod() > 0.f)
		if(IsOwnerActorAuthoritative())
			// 权威方仅本地执行
			PredictionKey = FPredictionKey();
		else
			// 非权威方不执行
			return FActiveGameplayEffectHandle();
	// 检查免疫 HasApplicationImmunityToSpec
	// 随机概率触发 Spec.GetChanceToApplyToTarget()
	// 需要的Tag和有就移除的Tag RequirementsMet RemovalTagRequirements
	// 本地预测的瞬时GE,后面由于需要校验、纠正,所以时间需要是Infinite
	bool bTreatAsInfiniteDuration = GetOwnerRole() != ROLE_Authority && PredictionKey.IsLocalClientKey() && Spec.Def->DurationPolicy == EGameplayEffectDurationType::Instant;
	// 非瞬时GE或本地预测的瞬时GE
	if (Spec.Def->DurationPolicy != EGameplayEffectDurationType::Instant || bTreatAsInfiniteDuration)
		// 执行,维护FActiveGameplayEffectsContainer(见下文)
		AppliedEffect = ActiveGameplayEffects.ApplyGameplayEffectSpec(Spec, PredictionKey, bFoundExistingStackableGE);
		// Modifier
		for (const FGameplayModifierInfo& Modifier : Spec.Def->Modifiers)
			Modifier.ModifierMagnitude.AttemptCalculateMagnitude(Spec, Magnitude);
	// 本地预测的瞬时GE改为InfiniteDuration
	if (bTreatAsInfiniteDuration)
		OurCopyOfSpec->SetDuration(UGameplayEffect::INFINITE_DURATION, true);
	// GameplayCue
	// 瞬时GE的执行
	if (bTreatAsInfiniteDuration)
	else if (Spec.Def->DurationPolicy == EGameplayEffectDurationType::Instant)
		ExecuteGameplayEffect(*OurCopyOfSpec, PredictionKey);
	// 添加后可能移除其它GE
	if (bIsNetAuthority)
		ActiveGameplayEffects.AttemptRemoveActiveEffectsOnEffectApplication(Spec, MyHandle);
	// 添加后添加其它GE
	ApplyGameplayEffectSpecToSelf(*TargetSpec.Data.Get(), PredictionKey);

预测GE

当本地预测Instant GE的时候

FActiveGameplayEffectHandle UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(const FGameplayEffectSpec &Spec, FPredictionKey PredictionKey)
	bool bTreatAsInfiniteDuration = GetOwnerRole() != ROLE_Authority && PredictionKey.IsLocalClientKey() && Spec.Def->DurationPolicy == EGameplayEffectDurationType::Instant;
	if (Spec.Def->DurationPolicy != EGameplayEffectDurationType::Instant || bTreatAsInfiniteDuration)
		AppliedEffect = ActiveGameplayEffects.ApplyGameplayEffectSpec(Spec, PredictionKey, bFoundExistingStackableGE);

删除GE的函数绑定到PKey,有两个触发入口:Reject和CaughtUp

FActiveGameplayEffect* FActiveGameplayEffectsContainer::ApplyGameplayEffectSpec(const FGameplayEffectSpec& Spec, FPredictionKey& InPredictionKey, bool& bFoundExistingStackableGE)
	AppliedActiveGE = new FActiveGameplayEffect(NewHandle, Spec, GetWorldTime(), GetServerWorldTime(), InPredictionKey);
	*PendingGameplayEffectNext = AppliedActiveGE;
	if (!(InPredictionKey.IsLocalClientKey() == false || IsNetAuthority()))
		InPredictionKey.NewRejectOrCaughtUpDelegate(FPredictionKeyEvent::CreateUObject(Owner, &UAbilitySystemComponent::RemoveActiveGameplayEffect_NoReturn, AppliedActiveGE->Handle, -1));
	void FActiveGameplayEffectsContainer::InternalOnActiveGameplayEffectAdded(FActiveGameplayEffect& Effect)
CaughtUp

如果本地预测成功,当客户端收到服务器下发的GE时,本地预测的GE就可以删除了(替代)。而收到了PKey的同步,就说明收到了服务器下发的GE

具体流程:

PKey使用FReplicatedPredictionKeyMap作为容器:

UPROPERTY(Replicated)
FReplicatedPredictionKeyMap UAbilitySystemComponent::ReplicatedPredictionKeyMap;

Params.Condition = COND_OwnerOnly;
DOREPLIFETIME_WITH_PARAMS_FAST(UAbilitySystemComponent, ReplicatedPredictionKeyMap, Params);

再看ScopedPredictionWindow的作用:在Scope结束后,恢复之前的PKey,并把当前的PKey加入ReplicatedPredictionKeyMap

FScopedPredictionWindow::FScopedPredictionWindow(UAbilitySystemComponent* AbilitySystemComponent, FPredictionKey InPredictionKey, bool InSetReplicatedPredictionKey /*=true*/)
	if (AbilitySystemComponent->IsNetSimulating() == false)
		Owner = AbilitySystemComponent;
		// 设置ScopedPredictionKey,存下原先的ScopedPredictionKey
		RestoreKey = AbilitySystemComponent->ScopedPredictionKey;
		AbilitySystemComponent->ScopedPredictionKey = InPredictionKey;
		ClearScopedPredictionKey = true;
		SetReplicatedPredictionKey = InSetReplicatedPredictionKey;

FScopedPredictionWindow::~FScopedPredictionWindow()
	if (SetReplicatedPredictionKey)
		// 记录已经结束的Scope的PKey
		if (OwnerPtr->ScopedPredictionKey.IsValidKey())
			// 加入环形数组并同步,这里是预测的关键
			OwnerPtr->ReplicatedPredictionKeyMap.ReplicatePredictionKey(OwnerPtr->ScopedPredictionKey);
				PredictionKeys[Index].PredictionKey = Key;
				MarkItemDirty(PredictionKeys[Index]);
			OwnerPtr->bIsNetDirty = true;
	// 恢复ScopedPredictionKey
	if (ClearScopedPredictionKey)
		OwnerPtr->ScopedPredictionKey = RestoreKey;

ReplicatedPredictionKeyMap同步时调用FReplicatedPredictionKeyItem的同步:

bool FReplicatedPredictionKeyMap::NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
	return FastArrayDeltaSerialize<FReplicatedPredictionKeyItem>(PredictionKeys, DeltaParms, *this);

FReplicatedPredictionKeyItem
	void PostReplicatedAdd(const struct FReplicatedPredictionKeyMap &InArray) { OnRep(); }
	void PostReplicatedChange(const struct FReplicatedPredictionKeyMap &InArray) { OnRep(); }
	
void FReplicatedPredictionKeyItem::OnRep()
	FPredictionKeyDelegates::CatchUpTo(PredictionKey.Current);
		for (auto& Delegate : DelPtr->CaughtUpDelegates)
			Delegate.ExecuteIfBound();

通过刚才绑定的CatchUp回调,移除GE

void RemoveActiveGameplayEffect_NoReturn(FActiveGameplayEffectHandle Handle, int32 StacksToRemove=-1)
	UAbilitySystemComponent::RemoveActiveGameplayEffect(Handle, StacksToRemove);
		bool FActiveGameplayEffectsContainer::RemoveActiveGameplayEffect(FActiveGameplayEffectHandle Handle, int32 StacksToRemove)
			InternalRemoveActiveGameplayEffect(ActiveGEIdx, StacksToRemove, true);

bool FActiveGameplayEffectsContainer::InternalRemoveActiveGameplayEffect(int32 Idx, int32 StacksToRemove, bool bPrematureRemoval)
	FActiveGameplayEffect& Effect = *GetActiveGameplayEffect(Idx);
	// 清除层数
	if (StacksToRemove > 0 && Effect.Spec.StackCount > StacksToRemove)
		Effect.Spec.StackCount -= StacksToRemove;
		return false;
	// 移除Tag和Magnitude
	InternalOnActiveGameplayEffectRemoved(Effect, ShouldInvokeGameplayCueEvent, GameplayEffectRemovalInfo);
	// 持续
	Owner->GetWorld()->GetTimerManager().ClearTimer(Effect.DurationHandle);
	// 周期
	Owner->GetWorld()->GetTimerManager().ClearTimer(Effect.PeriodHandle);
	// 移除后施加其它GE
	InternalApplyExpirationEffects(Effect.Spec, bPrematureRemoval);
Reject

在服务器拒接GA,导致本地预测失败时,在上述GA的预测流程中,预测失败后会广播该委托,后续流程同上

void UAbilitySystemComponent::ClientActivateAbilityFailed_Implementation(FGameplayAbilitySpecHandle Handle, int16 PredictionKey)
	if (PredictionKey > 0)
		FPredictionKeyDelegates::BroadcastRejectedDelegate(PredictionKey);
	Spec->ActivationInfo.SetActivationRejected();
	Ability->CurrentActivationInfo.SetActivationRejected();
	Ability->K2_EndAbility();

依赖性预测

例如一个前后两件事情存在依赖关系,A成功触发后,才能在某个时刻触发B。如果A预测失败,那边这种情况下B也应该被取消。
在PredictionKey内,有一个Base的字段,存的就是当前键依赖的键。
FPredictionKey::GenerateDependentPredictionKey,将自己变为子Key

// Key1生成Key2,Key2生成Key3
Key1. Base:0		Cur:New1
Key2. Base:New1		Cur:New2
Key3. Base:New1		Cur:New3

Key3的base是最开始的Key1,与Key2的依赖是手动调用FPredictionKeyDelegates::AddDependency进行的绑定,就是Reject Key2的时候,也会调用Reject Key3。
而Key2、Key3与Key1之间的依赖关系,走的Base这个字段。通过FPredictionKey::DependsOn判断是否需要Reject。但是我看代码的时候没有找到哪里触发了Key2的Reject。

ActiveGameplayEffects

ASC上存储GE的是

UPROPERTY(Replicated)
FActiveGameplayEffectsContainer ActiveGameplayEffects;

这个是Replicated的,有很多直接对ActiveGameplayEffects的操作,这些操作都需要在服务器进行,之后再同步到客户端,例如SetActiveGameplayEffectLevel


AttributeSet

在Character上新建

AttributeSet = CreateDefaultSubobject<UXXXAttributeSet>(TEXT("AttributeSet"));

ASC上有一份AttributeSet的指针数组

UPROPERTY(Replicated)
TArray<UAttributeSet*>	SpawnedAttributes;

UAbilitySystemComponent::InitializeComponent()的时候,从Owner上拉取所有的UAttributeSet成员加入到SpawnedAttributes内部

TArray<UAttributeSet*>& SpawnedAttributesRef = GetSpawnedAttributes_Mutable();
TArray<UObject*> ChildObjects;
GetObjectsWithOuter(Owner, ChildObjects, false, RF_NoFlags, EInternalObjectFlags::PendingKill);
for (UObject* Obj : ChildObjects)
	UAttributeSet* Set = Cast<UAttributeSet>(Obj);
	if (Set)  
		SpawnedAttributesRef.AddUnique(Set);
		bIsNetDirty = true;

属性通知

// 在瞬时、周期性执行的GE执行之前调用,可以修改、增加、屏蔽改Modifier
virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData &Data) { return true; }
// 在瞬时、周期性执行的GE执行后调用
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData &Data) { }
// Current值变化前回调,做Clamp之类的简单的值修改
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) { }
// Current值变化后回调
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) { }
// Base值变化前回调,做Clamp之类的简单的值修改
virtual void PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const { }
// Base值变化后回调 
virtual void PostAttributeBaseChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) const { }

Montage

ASC内部算是自己弄了一套Montage的同步,可以直接借用这个来同步蒙太奇。

播放

ASC内部专门弄了个结构体记录Montage信息:

/** Data structure for replicating montage info to simulated clients */
UPROPERTY(ReplicatedUsing=OnRep_ReplicatedAnimMontage)
FGameplayAbilityRepAnimMontage RepAnimMontageInfo;

这个结构体虽然会同步到所有客户端,但是只在模拟端有用(不清楚为什么不改成COND_SimulatedOnly)

DOREPLIFETIME_WITH_PARAMS_FAST(UAbilitySystemComponent, RepAnimMontageInfo, Params);

通过 ASC封装的PlayMontage接口,就可以完成蒙太奇同步到模拟端:

float UAbilitySystemComponent::PlayMontage(UGameplayAbility* InAnimatingAbility, FGameplayAbilityActivationInfo ActivationInfo, UAnimMontage* NewAnimMontage, float InPlayRate, FName StartSectionName, float StartTimeSeconds)
	Duration = AnimInstance->Montage_Play(NewAnimMontage, InPlayRate, EMontagePlayReturnType::MontageLength, StartTimeSeconds);
	LocalAnimMontageInfo.AnimMontage = NewAnimMontage;
	LocalAnimMontageInfo.AnimatingAbility = InAnimatingAbility;
	LocalAnimMontageInfo.PlayInstanceId = (LocalAnimMontageInfo.PlayInstanceId < UINT8_MAX ? LocalAnimMontageInfo.PlayInstanceId + 1 : 0);
	InAnimatingAbility->SetCurrentMontage(NewAnimMontage);
	// 是否是权威端
	if (ShouldRecordMontageReplication())
		FGameplayAbilityRepAnimMontage& MutableRepAnimMontageInfo = GetRepAnimMontageInfo_Mutable();
		// 更新数据
		MutableRepAnimMontageInfo.AnimMontage = NewAnimMontage;
		...
		// 更新数据
		AnimMontage_UpdateReplicatedData();
			OutRepAnimMontageInfo.AnimMontage = LocalAnimMontageInfo.AnimMontage;
			OutRepAnimMontageInfo.PlayRate = AnimInstance->Montage_GetPlayRate(LocalAnimMontageInfo.AnimMontage);
			OutRepAnimMontageInfo.Position = AnimInstance->Montage_GetPosition(LocalAnimMontageInfo.AnimMontage);
			OutRepAnimMontageInfo.BlendTime = AnimInstance->Montage_GetBlendTime(LocalAnimMontageInfo.AnimMontage);
	if (IsOwnerActorAuthoritative())
		// 马上执行属性同步
		AbilityActorInfo->AvatarActor->ForceNetUpdate();
	else
		// 如果在 ScopedPredictionKey 期间,说明本次播放是本地预测,绑定预测失败回调,取消动画
		FPredictionKey PredictionKey = GetPredictionKeyForNewAction();
		PredictionKey.NewRejectedDelegate().BindUObject(this, &UAbilitySystemComponent::OnPredictiveMontageRejected, NewAnimMontage);

// 每次调用都会 Mark_Dirty
FGameplayAbilityRepAnimMontage& UAbilitySystemComponent::GetRepAnimMontageInfo_Mutable()
	MARK_PROPERTY_DIRTY_FROM_NAME(UAbilitySystemComponent, RepAnimMontageInfo, this);
	return RepAnimMontageInfo;

因为这部分同步不涉及主控端,所以主控端需要自行调用PlayMontage。
UAbilityTask_PlayMontageAndWait其实就是只进行了PlayMontage,所以需要在主控端和服务器都调用。

至于模拟端,会根据收到的服务器数据,进行本地复刻:

void UAbilitySystemComponent::OnRep_ReplicatedAnimMontage()
	if (!AbilityActorInfo->IsLocallyControlled())			
		// New Montage to play
	if ((LocalAnimMontageInfo.AnimMontage != ConstRepAnimMontageInfo.AnimMontage) || 
		    (LocalAnimMontageInfo.PlayInstanceId != ConstRepAnimMontageInfo.PlayInstanceId))
		LocalAnimMontageInfo.PlayInstanceId = ConstRepAnimMontageInfo.PlayInstanceId;
		PlayMontageSimulated(ConstRepAnimMontageInfo.AnimMontage, ConstRepAnimMontageInfo.PlayRate);
	AnimInstance->Montage_SetPlayRate(LocalAnimMontageInfo.AnimMontage, ConstRepAnimMontageInfo.PlayRate);
	if (bReplicatedIsStopped)
		if (!bIsStopped)
			CurrentMontageStop(ConstRepAnimMontageInfo.BlendTime);
	...
	AnimInstance->Montage_SetPosition(LocalAnimMontageInfo.AnimMontage, ConstRepAnimMontageInfo.Position);

维护

ASC会Tick更新蒙太奇数据

void UAbilitySystemComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
	if (IsOwnerActorAuthoritative())
		AnimMontage_UpdateReplicatedData();

至于自己调用的修改,可以参考UAbilitySystemComponent::CurrentMontageJumpToSection,只在主控端调用即可:

void UAbilitySystemComponent::CurrentMontageJumpToSection(FName SectionName)
	// 本地执行
	AnimInstance->Montage_JumpToSection(SectionName, LocalAnimMontageInfo.AnimMontage);
	// 服务器更新数据同步模拟端
	if (ShouldRecordMontageReplication())
		...
		AnimMontage_UpdateReplicatedData();
	// 主控端调用Server
	if (!IsOwnerActorAuthoritative())
		ServerCurrentMontageJumpToSectionName(LocalAnimMontageInfo.AnimMontage, SectionName);

使用问题

项目遇到一个问题,技能使用的 ASC 的 PlayMontage,但是有一个其它的地方播放死亡蒙太奇(通过属性同步)。
同一帧执行了技能的蒙太奇播放和死亡蒙太奇播放,RepAnimMontageInfo 内部记录的是技能的蒙太奇,然后属性同步在下一帧 Tick 之前执行,也就是说 Tick 抓取本地蒙太奇信息还没有执行。
ASC 的蒙太奇信息同步和死亡蒙太奇播放信息同步,不能保证顺序,就有可能出现一个问题:先播放死亡蒙太奇,再播放技能蒙太奇。

所以,要么蒙太奇信息抓取放到 PreReplication,要么全都都使用 ASC 的 PlayMontage,注意 ASC 的 PlayMontage 传入的技能可以为空。


GameplayTag

除了GE内部的Tag管理外,可以自己通过UAbilitySystemBlueprintLibrary::Remove、AddLooseGameplayTags进行额外的Tag操作。注意,不要使用AddLooseGameplayTags、AddReplicatedLooseGameplayTags,除非自己清楚这里面的同步相关事情。

使用 GameplayTags.PrintReport 输出同步的各个 Tag 次数,对于高频 Tag 加入 Config 进一步减少带宽消耗。
在这里插入图片描述
在这里插入图片描述

参考这篇:https://zhuanlan.zhihu.com/p/652646177

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值