Hi! It’s me again, I promised a follow-up in my previous post, and here I am, with another Unreal Engine blog entry. Today we are going to cover a pretty obscure topic that to my knowledge hasn’t been covered in a practical manner anywhere. That is, network optimizations in GAS.
This post will revise some of the concepts explained in Q&A With Epic Game’s Dave Ratti and will expand further on them. Let’s get into it.
Introduction
If we take a look at question #5, we find the whole rationale about who should own the Ability System Component (ASC):
In general I would say anything that does not need to respawn
should have the Owner and Avatar actor be the same thing. Anything
like AI enemies, buildings, world props, etc.
Anything that does respawn should have the Owner and Avatar be
different so that the Ability System Component does not need to
be saved off / recreated / restored after a respawn.. PlayerState
is the logical choice it is replicated to all clients (where as
PlayerController is not). The downside is PlayerStates are always
relevant so you can run into problems in 100 player games.
- Dave Ratti
Let’s assume our game requires to preserve state between respawns for our controllable hero characters. We have two options:
Implement Actor Pooling and make the hero the ASC owner: If we never destroy our pawns there is no point on making the PlayerState the owner of the ASC. However implementing Actor Pooling can become complicated depending on the scope of the project.
Make the PlayerState the ASC owner: With this method it is not required to implement Actor Pooling to preserve state, since the PlayerState persists during the whole game.
Actor Pooling is a whole topic by itself, so we are going to leave it out of the formula for this post, but you can learn more about it in this video by YAGER.
Using the PlayerState as the owner
However, when using the PlayerState as the owner of the ASC there is a number of questions and constraints we must consider:
How do I initialize the ASC on the Pawn?
PlayerStates run at a reduced network frequency, that’s a no-no for a competitive game!
PlayerStates are always relevant! Poor network bandwidth! :(
In this article we’ll demonstrate how we can palliate these issues using multiple optimization techniques that we can find in our precious engine. But before we start, this article assumes the reader is using a Mixed Replication Mode in the PlayerState’s ASC.
How do I initialize the ASC on the Pawn?
To initialize the ASC on our Pawn we need to override three functions:
Within the scope of the functions we ensure the validity of the different components relevant for the initialization of the ASC. First let’s start with PossessedBy:
PossessedBy(AController* NewController) gets called on the server, and in this function we are setting the PlayerState as the owner of the ASC, and passing ARBPlayerCharacter as the Avatar.
Next, OnRep_PlayerState():
The implementation logic is equal to the one found in PossessedBy, except with some minor differences - ie. Abilities cannot be added in the client, Attributes can be added but require custom initialization logic, Gameplay Effects won’t work (I hope I can cover this in a future article).
And finally, OnRep_Controller():
This function is a fail-guard to the PlayerController data race we can encounter while initializing GAS. It might happen that the PlayerController wasn’t valid when OnRep_PlayerState() was called, therefore we need this extra override.
The setup isn’t very complicated, it is simply very decentralized. I did a PR recently trying to improve this by overriding SetPlayerState instead of OnRep_PlayerState and PossessedBy. (An upvote in the PR would be dope!!!)
As a final remark, I highly recommend to take a look at the LyraPawnExtensionComponent from Lyra to safely initialize your Pawns using an ability component. Their approach catches several use cases that aren’t probably observed in this article. Click here for more information.
If your ASC is on your PlayerState, then you will need to increase
the NetUpdateFrequency of your PlayerState. It defaults to a very
low value on the PlayerState and can cause delays or perceived lag
before changes to things like Attributes and GameplayTags happen on
the clients. Be sure to enable Adaptive Network Update Frequency,
Fortnite uses it.
- Dan
Increasing the NetUpdateFrequency of the PlayerState isn’t a very good idea, since the goal of this article revolves around optimizing the network usage while using GAS. So let’s take a look on how to activate the Adaptive Network Update Frequency.
The theory is simple, we should set net.UseAdaptiveNetUpdateFrequency to 1. For that, in the Config folder of your project, open
DefaultEngine.ini and add the following section:
However, in UE4 and UE5 Early Access, this won’t work. To make it work in those engine versions you have the following alternatives:
a) Remove net.UseAdaptiveNetUpdateFrequency=0 from Engine/Config/ConsoleVariables.ini in the Engine’s directory (like this).
b) Switch the CVAR value in the Server when the game starts (right after ConsoleVariables.ini gets called).
PlayerStates are always relevant
Yes indeed they are. And by design, there isn’t much we can do about this, but if we follow Dave Ratti’s advice, we could implement some optimizations (from the Q&A, question #3):
Fortnite goes a few steps further with its optimizations. It actually
does not replicate the UAbilitySystemComponent at all for simulated
proxies. The component and attribute subobjects are skipped inside
::ReplicateSubobjects() on the owning fortnite player state class. We
do push the bare minimum replicated data from the ability system
component to a structure on the pawn itself (basically, a subset of
attribute values and a white list subset of tags that we replicate down
in a bitmask). We call this a “proxy”. On the receiving side we take the
proxy data, replicated on the pawn, and push it back into ability system
component on the player state. So you do have an ASC for each player in
FNBR, it just doesn’t directly replicate: instead it replicates data via
a minimal proxy struct on the pawn and then routes back to the ASC on
receiving side. This is advantage since its A) a more minimal set of data
B) takes advantage of pawn relevancy.
- Dave Ratti
Let’s take a look on how to implement this.
1. The component and attribute subobjects are skipped inside ::ReplicateSubobjects() on the owning fortnite player state class
2. We do push the bare minimum replicated data from the ability system component to a structure on the pawn itself
By doing this we are considerably reducing the network bandwith used as we can decide which clients get our updates. Following next we present a plausible implementation:
2.1. Define the replication proxy struct
In this struct we are using an 8 bits BitMask (uint8) to identify 8 different gameplay tags. We can achieve the same effect using NetDeltaSerialize the same way it’s done in the FHitResult definition. This BitMask works like a set of booleans to represent whether certain tag exists or not in the target, is up to the game-code to interpret what each bit means.
Once the variable replicates, we can do the following to read its value on the client:
Note: To ease the explanation I’ve used binary notation to identify the flag position, but I recommend using hexadecimal for briefness.
2.2. Define getters
This is not needed if you don’t use Push Based replication model.
2.3. Pass-in the data
3. …and then routes back to the ASC on receiving side.
For that we’ll use an OnRep in the receiving end struct:
With the following implementation:
By doing this, the simulated proxies will gather the desired replicated data marked down in the ReplicationProxyVarList Struct.
4. Handling gameplay cues
Since we are skipping simulated proxies in ReplicateSubobjects, we must handle animations and GameplayCues manually, since they no longer replicate to everyone.
And this is a great change since we’ll be using the Pawn’s relevancy instead of the boring always relevant PlayerState.
1) The very first step is setting ReplicationProxyEnabled to true in the AbilitySystemComponent class.
2) Then, inherit UAbilitySystemReplicationProxyInterface and IAbilitySystemReplicationProxyInterface:
3) Following next, implement the full interface in our replication proxy class, in my case the ARBPlayerCharacter:
4) Now, be prepared to write boilerplate code. Starting with the header:
And continuing with the implementation:
5) And finally, override and add these functions to your Ability System Component:
Followed by the implementation:
And… we are done. With this we will be able to execute Gameplay Cues which will be shown to everyone using the Character relevancy.
Some tips
What a ride… well, to sum up I’d like provide some tips regarding the matter:
You probably don’t need to replicate that attribute.
Use the replication graph, it now exists!
Consider wisely if you really need these optimizations, the amount of boilerplate coded needed is significant.
The more boilerplate code you add, the greater the chances of introducing bugs into your codebase, be careful!
In this post we’ll learn a very cool trick to enhance your multiplayer logs. It consists on accessing the server instance from the client in the editor. But ...