Gameplay Ability System - Prediction on simulated proxy

Hello,

I have a simple use case where I’m trying to understand how the Gameplay Ability System works and what’s possible to do.

Setup:

I have a player character with the GAS component and two attributes handled via UAttributeSet - Health (HP) and Stamina. The health and stamina are visible on the HUD for the local player while the health of other players is visible as a health bar above their characters.

I’m playing the game with two clients and a dedicated server.

Case 1:

I want to implement a melee attack that uses stamina. I can detect the input on my local client and start a new ability, e.g. Ability_Attack. Assume the ability is simple, it just just applies a Gameplay Effect that reduces the stamina.

The ability will run on both my client and the server and apply the effect on both ends. On my local client, I will reduce my local stamina (prediction) while the server reduces the authoritative server version. The server version gets replicated back to my client and in case of a mismatch, the server has authority. The prediction ensures I get instant feedback on the stamina UI and I don’t need to wait for the server to replicate the new value.

This case works perfectly and it’s the other one that I’m having problems with.

Case 2:

I want the melee attack to deal damage to the other player. Assume the following naming: Client1 - my client that receives the inputs. Player1 - player controlled on Client1. Client2/Player2 is the other player.

I detect the melee attack collision locally, on Client1. I’m hitting the other player, Player2 and I want to reduce it’s HP. I can activate a new ability Ability_DoDamage that will run a simple Gameplay Effect on the server and reduce the HP of Player2. The new HP value will be replicated back to Client1 and the change will be visible on the health bar.

However, I would like to have instant feedback when I deal damage - the HP should also be reduced on my local instance of Player2 (simulated proxy) as soon as I decide to deal damage. Otherwise I need to wait for the ability to run on the server and the new HP value is replicated back to me, resulting in an obvious delay when observing the health bar.

Questions:

  • Is there a desired approach to this problem?
  • Is it possible to apply the Gameplay Effect on a simulated proxy?
  • Can I get the same behavior as with the stamina example where the value is predicted on the client and authoritative on the server?

I can solve this issue by adding an additional attribute, LocalHealth and implement a custom solution for handling two values but I’d like to know if there’s a proper, GAS solution to this.

Thank you!

[Attachment Removed]

Hey there, it’s possible to apply GameplayEffects predictively on simulated proxies. Some things to be aware of:

  • PredictTargetGameplayEffects has to be true in your project settings, which it is by default
  • Instant GEs are applied as Infinite when applied predictively, so they can be “tracked” to be removed. This is to be able to undo the effect if rejected.
  • There is a known issue with GameplayCues when applied predictively to a target. We don’t plan to fix this because predicted GEs on target is a low priority for us. The bug is that stateful GameplayCues are applied predictively, but when predicting a GE on a target the predicted GC is ended and a new GC is started. This results in some duplicate GC events: Active-Remove, Active-Remove, if you aren’t aware of this quirk. Predicted effects on yourself don’t have this issue.

However, all of this said, Epic doesn’t predictively apply GEs on targets. We disabled PredictTargetGameplayEffects in Fortnite and our other projects.

I think doing something like LocalHealth would give you the most control. Modifying attributes predictively is fine, but be sure not to predict one-way outcomes like starting off death animations, until a value is confirmed.

[Attachment Removed]

Hello,

Thank you for your answer.

Just to verify, you’re not using any kind of GE prediction (both on the autonomous and simulated proxies or you’re only using it on autonomous proxies?

Is there a specific reason you avoid it?

[Attachment Removed]

We only predict GE on autonomous proxies.

The reason we avoid it on simulated proxies, is because of the increased risk of misprediction, especially on other players.

If another player does an action that prevents the GE’s application, and the RPC for that action arrives on the server faster than your ability, then the server would reject the GE application, and a roundtrip amount of latency has been mispredicted for the sim proxy. Instead of mispredicting on the sim proxy, which could involve lowering sim proxy health, particle effects and more, we prefer to just wait for the server’s outcome. In our experience, especially in PvP games, players will often take actions (hiding behind cover, fast movement abilities, immunity abilities) that would prevent GE application.

We have considered predicting on sim proxies that are AI controlled, which increases responsiveness without punishing any player, but haven’t gotten around to experimenting with that idea in practice.

[Attachment Removed]

I understand, thank you for the insight. I would still like to make the simulated proxy prediction work so we can decide whether to use it or not depending on our use case.

I did some more experiments and managed to make it work (partially).

I’m still using the DealDamage GE which simply reduces the HP by 15 from the original example.

Server code:

FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(DealDamageEffect, 1);
SpecHandle.Data->SetSetByCallerMagnitude(DamageAmountTag, -Data->DamageAmount); // DamageAmount = 15
Target->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());

Simulated proxy:

FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(DealDamageEffect, 1);
SpecHandle.Data->SetSetByCallerMagnitude(DamageAmountTag, -Data->DamageAmount); // DamageAmount = 15
Target->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get(), FPredictionKey::CreateNewPredictionKey(Target));

This is the problem I’m seeing now:

-On SimulatedProxy the effect is applied predictively as Inifinite and it only changes the Current value of the HP attribute: Health = Base:100 Current:85

-On Server the effect is applied as Instant and it changes both the Base and Current values: Health = Base:85 Current:85

After the value is replicated from the server to the SimulatedProxy, it results in Health = Base:85 Current:70. It seems to apply the locally existing (Base - Current) difference on top of the value received from the server.

Is there a way around this?

[Attachment Removed]

Regarding this:

“On SimulatedProxy the effect is applied predictively as Inifiniteand it only changes the Current value of the HP attribute: Health = Base:100 Current:85

This is intentional to make the supported autonomous proxy use case work. When you predictively apply a GE to yourself as autonomous proxy, we will apply the GE and its modifiers as Infinite, so that it can be removed later as soon as the server has applied the GE as well. So we apply the instant GE as infinite, just to remember that it exists and to remove it later, removing the mods and tags. That’s also why the Health’s Current value is modified instead of the Base value, because we treat the predicted GE as a duration-based, undoable GE instead of a permanent one. Ultimately whether the Base (permanent) or Current (temporary) value is modified doesn’t matter, as long as the predicted GE is removed so the same modifier hasn’t applied to both.

With the usual autonomous proxy predicted GE (for example: a local predicted ability calls ApplyGameplayEffectToOwner), the predicted GE sets up cleanup upon server confirmation in these functions. Preparing the delegate to remove the predicted GE on server confirmation:

>	UnrealEditor-GameplayAbilities.dll!FPredictionKey::NewRejectOrCaughtUpDelegate(TDelegate<void __cdecl(void),FDefaultDelegateUserPolicy> Event) Line 247	C++
 	UnrealEditor-GameplayAbilities.dll!FActiveGameplayEffectsContainer::ApplyGameplayEffectSpec(const FGameplayEffectSpec & Spec, FPredictionKey & InPredictionKey, bool & bFoundExistingStackableGE) Line 4486	C++
 	UnrealEditor-GameplayAbilities.dll!UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(const FGameplayEffectSpec & Spec, FPredictionKey PredictionKey) Line 1054	C++
 	UnrealEditor-GameplayAbilities.dll!UGameplayAbility::ApplyGameplayEffectSpecToOwner(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle) Line 2080	C++
 	UnrealEditor-GameplayAbilities.dll!UGameplayAbility::ApplyGameplayEffectToOwner(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const UGameplayEffect * GameplayEffect, float GameplayEffectLevel, int Stacks) Line 2056	C++
 	UnrealEditor-GameplayAbilities.dll!UGameplayAbility::BP_ApplyGameplayEffectToOwner(TSubclassOf<UGameplayEffect> GameplayEffectClass, int GameplayEffectLevel, int Stacks) Line 2041	C++
 	UnrealEditor-GameplayAbilities.dll!UGameplayAbility::execBP_ApplyGameplayEffectToOwner(UObject * Context, FFrame & Stack, void * const Z_Param__Result) Line 129	C++

This is the callstack for the client removing the predicted GE on an autonomous proxy:

>	UnrealEditor-GameplayAbilities.dll!FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers(const FActiveGameplayEffect & Effect, bool bInvokePredictedEffects, bool bPredictionRejected) Line 4893	C++
 	UnrealEditor-GameplayAbilities.dll!FActiveGameplayEffectsContainer::InternalOnActiveGameplayEffectRemoved(FActiveGameplayEffect & Effect, bool bInvokePredictedEffects, const FGameplayEffectRemovalInfo & GameplayEffectRemovalInfo) Line 4878	C++
 	UnrealEditor-GameplayAbilities.dll!FActiveGameplayEffectsContainer::InternalRemoveActiveGameplayEffect(int Idx, int StacksToRemove, bool bPrematureRemoval, bool bPredictionRejected) Line 4812	C++
 	UnrealEditor-GameplayAbilities.dll!FActiveGameplayEffectsContainer::RemoveActiveGameplayEffect(FActiveGameplayEffectHandle Handle, int StacksToRemove, bool bPredictionRejected) Line 4695	C++
 	UnrealEditor-GameplayAbilities.dll!UAbilitySystemComponent::RemoveActiveGameplayEffect_AllowClientRemoval(FActiveGameplayEffectHandle Handle, int StacksToRemove) Line 1248	C++
 	[External Code]	
 	[Inline Frame] UnrealEditor-GameplayAbilities.dll!TDelegate<void __cdecl(void),FDefaultDelegateUserPolicy>::ExecuteIfBound() Line 643	C++
 	UnrealEditor-GameplayAbilities.dll!FPredictionKeyDelegates::CatchUpTo(short Key) Line 333	C++
 	UnrealEditor-GameplayAbilities.dll!FReplicatedPredictionKeyItem::OnRep(const FReplicatedPredictionKeyMap & InArray) Line 602	C++
 	[Inline Frame] UnrealEditor-GameplayAbilities.dll!FReplicatedPredictionKeyItem::PostReplicatedChange(const FReplicatedPredictionKeyMap &) Line 585	C++
 	UnrealEditor-GameplayAbilities.dll!FFastArraySerializer::TFastArraySerializeHelper<FReplicatedPredictionKeyItem,FReplicatedPredictionKeyMap>::PostReceiveCleanup<TMap<int,TMap<int,FGuidReferences,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<int,FGuidReferences,0>>,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<int,TMap<int,FGuidReferences,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<int,FGuidReferences,0>>,0>>>(FFastArraySerializer::FFastArraySerializerHeader & Header, TArray<int,TSizedInlineAllocator<8,32,TSizedDefaultAllocator<32>>> & ChangedIndices, TArray<int,TSizedInlineAllocator<8,32,TSizedDefaultAllocator<32>>> & AddedIndices, TMap<int,TMap<int,FGuidReferences,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<int,FGuidReferences,0>>,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<int,TMap<int,FGuidReferences,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<int,FGuidReferences,0>>,0>> & GuidMap) Line 1172	C++
 	UnrealEditor-GameplayAbilities.dll!FFastArraySerializer::FastArrayDeltaSerialize_DeltaSerializeStructs<FReplicatedPredictionKeyItem,FReplicatedPredictionKeyMap>(TArray<FReplicatedPredictionKeyItem,TSizedDefaultAllocator<32>> & Items, FNetDeltaSerializeInfo & Parms, FReplicatedPredictionKeyMap & ArraySerializer) Line 1858	C++
 	UnrealEditor-GameplayAbilities.dll!FFastArraySerializer::FastArrayDeltaSerialize<FReplicatedPredictionKeyItem,FReplicatedPredictionKeyMap>(TArray<FReplicatedPredictionKeyItem,TSizedDefaultAllocator<32>> & Items, FNetDeltaSerializeInfo & Parms, FReplicatedPredictionKeyMap & ArraySerializer) Line 1215	C++
 	UnrealEditor-Engine.dll!UScriptStruct::ICppStructOps::NetDeltaSerialize(FNetDeltaSerializeInfo & DeltaParms, void * Data) Line 1923	C++

In that last callstack, the property being replicated that triggers the cleanup is ReplicatedPredictionKeyMap in AbilitySystemComponent. The process is explained in GameplayPrediction.h, see the comment starting from “If ServerTryActivateAbility succeeds”.

If you want to get predicted GEs on simulated proxies to work, you need to set up something similar, so:

  • You apply a GE predictively on a target, in whatever way you can where the ASC doesn’t ignore the application. Instant GEs should be applied as Infinite, for undo purposes.
  • You apply the same GE on the server.
  • The client finds out ASAP when the server has applied the same GE (or rejected the application) and removes the predicted Infinite GE.

You can try to make the above work with the ASC api with engine modifications, or the FPredictionKey api, or manually. Since this isn’t something we support, the rest is up to you. But I hope this information is helpful!

[Attachment Removed]