55 minute read

As you keep practicing online multiplayer in Unreal Engine, you might have already wondered what are the best practices to tackle a specific issue, or even ranted about your code not behaving the way you desired. In this post I try to consolidate all the tips and tricks I’ve gathered while revamping my humble networking experience.

By no means these are rules that you will be judged for if you choose not to comply with. However, they proved useful over the course of my experience with networking code in Unreal Engine.

1. Get to Know the Game Framework Objects Online-wise

Regardless of networking, understanding Unreal’s Gameplay Framework, will not only make your code well-structured, but also keep you from reinventing the wheel, which will save you precious time down the line.

Now multiplayer-wise, game framework Objects, and more specifically Actors, can behave differently over the network. While one Actor will always be relevant to all clients, another will always be relevant only to its owning client, while another not relevant to clients at all. These don’t mean much for single-player fanatics, but it should mean a lot for their multiplayer counterparts. It’s not just relevancy, but other replication settings may vary. In fact, relevancy and most other replication settings don’t mean anything if the Actor in question isn’t set to replicate. The following table sheds some light on some of those most notable replication settings variances on a bunch of different types of replicable Actors:

Actor Class Only Relevant To Owner? Always Relevant? Replicate Movement? Net Update Frequency Net Priority
GameState   :heavy_check_mark:   10.0 1.0
PlayerState   :heavy_check_mark:   1.0 1.0
PlayerController :heavy_check_mark:     100.0 3.0
Pawn     :heavy_check_mark: 100.0 3.0
Actor       100.0 1.0

Now let’s understand what these settings mean:

  1. bOnlyRelevantToOwner: If true, this Actor is only relevant to its owning client. If this flag is changed during play, all non-owner channels (those belonging to simulated proxies) would need to be explicitly closed.
  2. bAlwaysRelevant: Always relevant for all clients (overrides bOnlyRelevantToOwner).
  3. bReplicateMovement: If true, replicate movement/location related properties.
  4. NetUpdateFrequency: How often (per second) this Actor will be considered for replication, used to determine NextUpdateTime.
  5. NetPriority: Priority for this Actor when checking for replication in a low bandwidth or saturated situation, higher priority means it is more likely to replicate.

I highly suggest you read further about the Detailed Actor Replication Flow.

2. Beware of GetPlayerXXX(0) Static Functions

I keep seeing peeps using the following static functions in online multiplayer:

Bad Nodes

And their corresponding native versions:

  • UGameplayStatics::GetPlayerController(const UObject* WorldContextObject, int32 PlayerIndex).
  • UGameplayStatics::GetPlayerCharacter(const UObject* WorldContextObject, int32 PlayerIndex).
  • UGameplayStatics::GetPlayerPawn(const UObject* WorldContextObject, int32 PlayerIndex).
  • UGameplayStatics::GetPlayerState(const UObject* WorldContextObject, int32 PlayerStateIndex).
  • UGameplayStatics::GetPlayerCameraManager(const UObject* WorldContextObject, int32 PlayerIndex).

These functions will do you more harm than good, unless you know what you’re doing (which, in most cases, sadly ends up not being the case) or that you’re doing local multiplayer (split-screen) only, then you’re good to go.

I won’t really dwell on the why any long, but for example calling GetPlayerController(0) or the corresponding native UGameplayStatics::GetPlayerController(GetWorld(), 0) will end up returning different results based on the context, usually being:

  • Listen-server: The listen-server’s PlayerController.
  • Dedicated-server: The first client’s PlayerController.
  • Client: The client’s PlayerController.

The difference in behavior based on context is not a clear matter for beginners. In addition to that, there is almost no need to get that Actor by index, as indices won’t be consistent across different clients and servers. Also, the order for the PlayerController iterator is not consistent across map transfer so we don’t want to use that index. 99% of the time people pass in index 0 and want the primary local PlayerController, while sometimes in a listen-server environment for example, the client connects fast enough (through seamless travel) that it ends up as index 0 on the listen-server, so you end up with an undesired PlayerController, while expecting the listen-server’s PlayerController. Instead, using UGameInstance::GetFirstLocalPlayerController() will safely retrieve you the desired primary local PlayerController. Sadly, it’s not exposed to Blueprint, but you can keep reading below for the exposed nodes.

That’s why I highly suggest you refrain from using these functions as there are always other ways to retrieve the Actor in question.

Depending on the context (from which class you are trying to retrieve the Actor in question) the alt functions/properties will vary, but here are the most handy:

Good Nodes

And their corresponding native versions:

  • AActor::GetOwner()
  • UActorComponent::GetOwner()
  • APawn::GetController()
  • AController::GetPawn()
  • APlayerState::GetPlayerController()
  • UUserWidget::GetOwningPlayer()
  • AHUD::GetOwningPlayerController()
  • APlayerCameraManager::GetOwningPlayerController()
  • APlayerState::GetPawn()
  • AController::GetPlayerState()
  • APlayerController.PlayerCameraManager

3. Be Aware that Blueprint-only Multiplayer is Limited

Most people that start doing Unreal Engine in general, will start coding in Blueprint. Without a shadow of doubt this also applies to those starting with multiplayer. While Blueprint-only multiplayer is fine, and can be sufficient to your simple needs at first, you will quickly come to realize that it’s limited. Let’s discover robust tools that Blueprint lacks:

  • Replication Graph: While it will most probably be replaced with Iris, it’s still worth to know that you won’t be able to utilize it in place of the old regular relevancy system if you’re a Blueprint-only project. It won’t reduce the number of replicated Actors, but will result in the server having to evaluate less Actors per data channel, so indeed it’s an improved relevancy mechanism that aims to optimize server cpu usage, as well as network bandwidth usage.

  • Custom Network Serialization for Types: Basically in C++ you are able to customize the way specific types are serialized over the network. Usually, to do so we simply wrap such types in a Struct, and implement NetSerialize() internally. Effectively speaking, this also helps in achieving atomic replication.

  • Fast TArray Replication - FTR: It’s a sophisticated Unreal built-in container that are usually preferred over the regular TArray for replicating large TArrays of Structs. It aims to lower down the server cpu time needed to check elements for replication every time the array gets dirtied. You also get a very handy replication callbacks on clients per element that gets added, removed, or even changed.

  • Essential Virtual Methods: It’s a matter of time to realize how limiting this C++ only feature can be. Here are the most notable virtual functions that you want to override almost in any real world project:
    • AActor::IsNetRelevantFor(): This lets you set your own criteria to whether an Actor is relevant for a specific network connection.
    • AController::SetPawn(): Great place to do extra Pawn initialization that both server and client need to do.
    • AController/APawn::OnRep_PlayerState(): Replication callback to when our PlayerState has replicated. In Blueprint you will have to do very dirty workarounds and hacks.
  • Proper Replication Callbacks: Basically the way OnRep/RepNotify functions behave in Blueprint is somehow different than the way they do in C++, and that’s basically by design. In Blueprint they are usually called locally every time the variable is set, with no respect to where that was done (server vs. client) and that’s definitely not a replication callback. However, their C++ counterparts are real replication callbacks that will fire only on client and only when set on server. This article is the most inclusive source that explains most of their nuances. In addition, C++ OnRep has a function overload that has pre-replication value as an argument, which can be very handy: AMyActor::OnRep_MyReplicatedProperty(MyType OldValue).

  • Fine Grain Control Over Replication Conditions: By default, both in C++ and Blueprint, a RepNotify/OnRep property set to the same old value on the server, won’t trigger the replication callback on clients (Blueprint will still trigger it on server, because as stated earlier above, it’s by design). However, if we wanted to change that behavior, and make it so that the replication callback always triggers on clients even if the newly set value was unchanged, we can do the following:

      void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
      {
          Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
          DOREPLIFETIME_CONDITION_NOTIFY(ThisClass, MyRepNotifyProperty, COND_None, REPNOTIFY_Always);
      }
    
  • Struct Property Replication Exclusion: C++ Structs have the ability to exclude some of their members from replication, while their Blueprint counterparts don’t. Here’s how you do it:

      USTRUCT()
      struct FMyStruct
      {
          GENERATED_BODY()
    
          UPROPERTY()
          float ReplicatedProperty;
    
          UPROPERTY(NotReplicated)
          float NotReplicatedProperty;
      }
    
      UCLASS()
      class MyProject_API AMyActor : public AActor
      {
          GENERATED_BODY()
    
          UPROPERTY(Replicated)
          FMyStruct MyReplicatedStructProperty;
      }
    
  • Replicate Subobjects that are not ActorComponents: UObjects are preferred than Actors/ActorComponents to replicate, for the simple reason that they are more lighter-weight. In C++ there are at least two ways to make these kind of Objects support replication, however in Blueprints there are none.

And the list goes on…

4. Read through the Source Code

You should be aware that Unreal’s source code is available and should be in the palm of your hand. It should be your number one of reliable info about anything Unreal Engine related.

It’s meant to help you:

  • Understand the engine’s infrastructure.
  • Better understand why and where things went wrong.
  • Ask better and smarter questions.

5. Filter Client-Server Execution Paths

For functions that activate on both server and client, there are a couple of handy methods for filtering their execution:

5.1 Filter by NetRole

For every Actor in the World, usually either the server or a client will have authority over that Actor.

For every Actor that exists on the server, the server has authority over that Actor - including all replicated Actors, and pre-placed (not specifically replicated) Actors. As a result, when AActor::HasAuthority() function is run on a client, and the target is an Actor that was replicated to them, it will return false. You can also use the Switch Has Authority convenience macro in Blueprint as a quick means to branch for different server and client behavior in replicated Actors:

Filter by NetRole

For every Actor that exists only on client, only that client will have authority over that Actor - including mainly cosmetic (zero effect to gameplay) Actors.

This method makes use of Network Roles:

EngineBaseTypes.h

/** The network role of an actor on a local/remote network context */
UENUM()
enum ENetRole
{
    /** No role at all. */
    ROLE_None,
    /** Locally simulated proxy of this actor. */
    ROLE_SimulatedProxy,
    /** Locally autonomous proxy of this actor. */
    ROLE_AutonomousProxy,
    /** Authoritative control over the actor. */
    ROLE_Authority,
    ROLE_MAX,
};

5.2 Filter by NetMode

This method makes use of Network Modes:

EngineBaseTypes.h

/**
 * The network mode the game is currently running.
 * @see https://docs.unrealengine.com/latest/INT/Gameplay/Networking/Overview/
 */
enum ENetMode
{
    /** Standalone: a game without networking, with one or more local players. Still considered a server because it has all server functionality. */
    NM_Standalone,

    /** Dedicated server: server with no local players. */
    NM_DedicatedServer,

    /** Listen server: a server that also has a local player who is hosting the game, available to other players on the network. */
    NM_ListenServer,

    /**
     * Network client: client connected to a remote server.
     * Note that every mode less than this value is a kind of server, so checking NetMode < NM_Client is always some variety of server.
     */
    NM_Client,

    NM_MAX,
};

By calling GetNetMode() and IsNetMode(), you can reliably decide what NetMode the Actor has, by chaining back to its owning World and the instance of the game that has it, which can have any of the previous network modes.

5.3 NetMode vs. NetRole

While most of the times you could opt for either one to get the same results, this is how I use them:

  • NetMode: I use this method in the earlier life points of an Actor, so any point before AActor::PreInitializeComponents() is called, as the network roles have not been setup properly yet.

  • NetRole: I use this method at any point in time after AActor::PreInitializeComponents() (inclusive) is called. In theory, this method is faster than the previous, and thus should be preferred when possible.

In short, NetMode is more reliable, while NetRole is faster.

6. Filter Owning Client Execution Paths

Filtering execution based on whether it is relevant to the owning client is another useful method of execution filtering for functions/events that activate on both owning and non-owning clients.

Here are the most remarkable scenarios where you want to utilize this type of filtering:

  • Adding UserWidgets to the screen.
  • Spawning visual effects.
  • Playing sound effects.

The filtering will vary based on the context, but here are the most handy methods you will want to use:

  • APawn::IsLocallyControlled()
  • APawn::IsLocallyViewed()
  • UGameplayAbility::IsLocallyControlled()
  • UAbilityTask::IsLocallyControlled()
  • AController::IsLocalController()
  • AController::IsLocalPlayerController()

Note: Not all of them are exposed to Blueprint, but the ones you will use most of the time are.

Warning: Avoid using these functions during spawning, and most notably during BeginPlay, as it is possible for a Pawn not to have a Controller assigned during construction.

6.1 Example Usage

A very common scenario in which you see this method of execution filtering being used, is if you want to add a 3D interaction widget upon overlapping with an Actor. You don’t want other players that have your simulated Pawn to see the interaction widget!

Filter by Owning Client

Note: AActor.Instigator works, since Pawn is always the instigator of its own. See APawn::PreInitializeComponents() for reference.

7. Refrain from Using Delay to Account for Replication Time

You should have already been in a situation where you are trying to retrieve a specific replicated property, usually being an Actor, and unluckily getting a null value. After some digging you figured that the property didn’t have enough time to replicate to be valid. The amount of time it takes for a property to replicate to clients in Unreal Engine can be affected by several factors, including RTT (also referred to as ping), the complexity of the Object being replicated, the number of replicated properties, and the replication settings being used. At which point, you falsely realize that you should delay your retrieval, which also seemingly stinks.

The most common real life example I’ve seen for this delay behavior, is before accessing PlayerState client-side, which is known to be a replicated property in a couple of game framework Actor classes. This is due to the fact that PlayerState has a low NetUpdateFrequency by default, which in simple words mean that potentially it will take the PlayerState Actor more time to replicate than its game framework counterparts. This situation leads a lot of novice developers to use the Delay node (or its native function, but it mostly comes from Blueprint-only developers), which is not a real clean fix, as you can never reliably predict how much time it’s going to take replication.

The real clean fix is to “listen” to when PlayerState replicates as a whole, or when one of its specific properties does, from various different places based on the various specific needs:

  • APawn::OnRep_PlayerState(): This gets called automatically on client-only (server excluded) as any other OnRep function does, and each time a Pawn is assigned a new PlayerState, and that by default happens when the Pawn is possessed by a PlayerController that owns the PlayerState in question. Note that usually you can’t count on this call, especially when you want to listen to a specific property that can keep changing and thus replicating throughout the game. However, it can prove useful at the life start of the Pawn. Note that a Pawn can get destroyed and spawned multiple times on the various client instances due to relevancy for example, and in which this replication callback will be called. All properties set before the previous circumstances occurred, should have potentially replicated by the time this function is called (with the exception of unmapped properties, i.e. properties that are assigned NetworkGUIDs and those usually being UObjects* and FNames that are not hardcoded in UnrealNames.inl).

  • AController::OnRep_PlayerState: This is usually less used than the previous, however it’s good to know that it’s similar in use, as it gets called at the life start of the PlayerController, or after a persistent PlayerController finishes seamless traveling.

  • APlayerState::BeginPlay() client-side: This is usually used by Blueprint-only fanatics, as they have no access to the previous two methods which are C++ only. Filtering for client execution path means that we are looking for the replicated client version of the PlayerState.

  • Busy-wait polling: Checking if PlayerState is valid on Tick, or rather using the RetriggerableDelay node, and then checking validity is an easy yet dirty fix. This is traditionally useful while prototyping in Blueprint.

  • AMyPlayerState::OnRep_MyProperty(): This is the preferred mostly used method, as it helps you track when each and every property gets replicated, after it potentially changes.

Usually peeps want for example UI to listen to when these replication callbacks take place, so UI can be updated accordingly. At that point, they should bind to a delegate where needed, and call it in either one of the functions above. In C++ they are called Delegates while in Blueprint they are called Event Dispatchers, with the Blueprint version being a specific of the various C++ candidates.

8. Know when is it Safe to Call RPCs

I often see peeps use RPCs (Remote Procedure Calls - also called replicated functions) and wondering why they are not called (more accurately processed/executed) in the desired FunctionCallspace. RPCs that are called on one instance of the game, could end up being:

  • Absorbed: Not executed at all.
  • Executed remotely: Executed on remote instance/s of the game.
  • Executed locally: Executed on the local instance of the game where it was called.
  • Executed locally and remotely

RPCs that are not called safely, will end up either being absorbed, executed locally and becoming meaningless, or even lost.

8.1 Absorbed RPC: No Owning Connection for Actor

This issue applies to Server RPCs called on/from an Actor that is not owned by a NetConnection (i.e. not owned by a player/client), such that they will not be processed and you would see the following very similar warning either in the Output Log window or inside one of the log files inside YourProjectName/Saved/Logs:

  
LogNet: Warning: UNetDriver::ProcessRemoteFunction: No owning connection for actor BP_MyActor_C_UAID_E0D55EF827881F5A01_1104206655. Function Server RPC will not be processed.

This happens in one of the two cases below, where in the former the Actor in question is not owned by client and is neither meant to be, while in the latter it’s meant to be so, but the timing of the RPC call happens before the Actor is owned properly by client.

Note: The native function that returns an Actor’s NetConnection is AActor::GetNetConnection(), and is overridden in APawn and APlayerController.

8.1.1 Actor shouldn’t have NetConnection

The only Actor which has NetConnection by default is PlayerController. Any other Actors that are owned by the PlayerController, either directly or indirectly, will have NetConnection too, with those usually being: Pawn, PlayerState. As a result, it’s usually safe to fire Server/Client RPCs from the former three Actors (we’ll see that’s not always the case due to timing).

While in theory one could own every Actor they want to Server/Client RPC from, that is a very bad approach, for a couple of reasons:

  • Different clients won’t be able to interact with the same Actor simultaneously, considering an Actor can have only one owner at a time.
  • You could end up in owning too many Actors, which results in too many unnecessary hard references.
  • It’s counter-intuitive to polymorphism which is perfect for this situation.
8.1.1.1 Example Usage

A very common example to the situation above, is if the client wants to interact with an Actor. In such case here’s the basic procedure the client should go by:

Input (player presses a key to interact with an Actor)  ServerInteract (Server RPC in PlayerController/Pawn)  Interact (interface function implemented in the Actor being interacted with)

8.1.2 Bad Timing: Actor has no NetConnection yet

While this issue seems similar to the previous in shape, it’s in fact different in essence. This one goes over Actors that are supposed to have NetConnection, such as: PlayerController/PlayerState/Pawn, where the RPC still gets dropped/absorbed. The reason is that the RPC call happens at a time where the Actor in question hasn’t been owned yet by a NetConnection.

Often times I see the following unfortunate code in a custom Blueprint Character class:

Bad Server RPC

Note: The code above is for demonstration purposes only and you should almost never need to RPC on an event (BeginPlay in our case) that fires on both server and client. Filtering client-server execution paths, would save us calling the meaningless, unnecessary RPC.

While the Server RPC could potentially execute fine on the server, due to the fact that the owning PlayerController (which has NetConnection) is created before the Pawn, so usually it replicates first. However, that’s not always the case given that these two Actors could have different net stats, such as update frequency and priority, or even due to packet loss.

Note: Don’t let functions like AActor::HasNetOwner() fool you into thinking that it means that the Actor is guaranteed capable of firing meaningful RPCs, because it is not. In the situation above, the PlayerController has net owner, but has no NetConnection. Therefore, this function indicates that the Actor is potentially capable of firing meaningful RPCs. However, the one that does guarantee you that is the previously mentioned AActor::GetNetConnection(), after it returns a non-null value.

Another common, yet unfortunate absorbed Client RPC that is seen in a custom Blueprint PlayerState class:

Bad Client RPC PlayerState

Note: The code above is for demonstration purposes only and you should almost never need to RPC on an event (BeginPlay in our case) that fires on both server and client. Filtering client-server execution paths, would save us calling the meaningless, unnecessary RPC.

Although the PlayerState is owned by a PlayerController at this point, the latter still has no owning Player (another way of saying NetConnection as it is a subclass of Player), and that is because APlayerState::BeginPlay() takes place before AGameModeBase::PostLogin() gets the chance to, in which it’s guaranteed a Player has been associated with a PlayerController, enabling it to call RPCs without them being absorbed.

8.2 Local RPC

Another important factor to decide if it’s safe to call any type of RPC at a specific point in time, is to logically decide if that point in time is safe or not.

For example, AGameModeBase::PostLogin() is the first place it is safe to call replicated functions on PlayerController.

Often times I see the following bad code in a custom Blueprint Character class:

Bad Client RPC

Note: The code above is for demonstration purposes only and you should almost never need to RPC on an event (BeginPlay in our case) that fires on both server and client. Filtering client-server execution paths, would save us calling the meaningless, unnecessary RPC.

With the following string printed to the Output Log, running a dedicated server that has one connected client:

LogBlueprintUserMessages: [BP_ThirdPersonCharacter_C_0] Server: Client RPC executed!

You could tell it’s not what we expected, since it should have said “Client:” instead of “Server:”. Meaning, the Client RPC, which we call on the server and expect to execute on client, is executing locally on server. Reason for that, is at the time BeginPlay executes, there is no such guarantee that the Pawn/Character has an owning connection (not that it’s even owned yet, which you can verify by printing the value of function AActor::GetOwner()), which by the table of RPC invoked from the server means that it runs on server, making it totally undesirable and meaningless.

We can alleviate the issue with this bad hotfix:

Bad Client RPC Hotfix

With the following desired string printed to the Output Log, running a dedicated server that has one connected client:

LogBlueprintUserMessages: [BP_ThirdPersonCharacter_C_0] Client 1: Client RPC executed!

However here’s the more proper fix:

Client RPC Real Fix

Reason we use Possessed event, is that it executes on the server when we are guaranteed to have been possessed by a Controller, which automatically calls AActor::SetOwner() function for us, letting the Controller own the Pawn, so it chains back to a NetConnection and is able to fire Client RPCs.

With all that being said, it’s not a good practice to call unnecessary RPCs to force an execution path on the other end (owning-client execution path in our example), when there’s a native event that is already implemented for us that executes on that end. In our case there are APawn::ReceiveControllerChanged() and APawn::ReceiveRestarted() BIEs (Blueprint Implementable Events), or their native caller versions APawn::NotifyControllerChanged() and APawn::NotifyRestarted() which occur on possession, and are called on both server and owning client.

8.3 Lost RPC

Let’s look at the following common problematic scenario that could occur in a custom GameState class for example:

Lost RPC

Note: The code above is for demonstration purposes only and you should almost never need to RPC on an event (BeginPlay in our case) that fires on both server and client. Filtering client-server execution paths, would save us calling the meaningless, unnecessary RPC.

With the following string printed to the Output Logrunning a dedicated server that has a connecting client:

LogBlueprintUserMessages: [BP_GameState_C_0] Server: Welcome!

As you might have expected, the client hasn’t been welcomed, as it was not there in the first place when the Multicast RPC took place. Notice that the RPC was lost even though it is was marked Reliable, and that’s because of the lossy nature of RPCs in general, as they simply don’t count for a pending NetConnection.

There are multiple options you can consider but I will route you to the native method that you could override and utilize:

GameModeBase.h

/** 
 * Allows game to send network messages to provide more information to the client joining the game via NMT_GameSpecific
 * (for example required DLC)
 * the out string RedirectURL is built in and send automatically if only a simple URL is needed
 */
virtual void GameWelcomePlayer(UNetConnection* Connection, FString& RedirectURL);

8.4 The Good News

Starting in UE 5.1, a new more intuitive and flexible way of replicating subobjects was added. One of the methods that was added is the following:

ActorComponent.h

/**
 * ReadyForReplication gets called on replicated components when their owning actor is officially ready for replication.
 * Called after InitializeComponent but before BeginPlay. From there it will only be set false when the component is destroyed.
 * This is where you want to register your replicated subobjects if you already possess some.
 * A component can get replicated before HasBegunPlay() is true if inside a tick or in BeginPlay() an RPC is called on it.
 * Requires component to be registered, initialized and set to replicate.
 */
virtual void ReadyForReplication();

As its name and the documentation suggest, this handy function is called when the ActorComponent is ready to start replicating properties, and call RPCs through its owner’s ActorChannel. It’s also stated that this function is called before UActorComponent::BeginPlay() is called, and also when the owning Actor is ready for replication, which gets us to the conclusion:

Actors with NetConnection will be able to fire RPCs meaningfully as early as AActor::BeginPlay() is called, solving the common issue relating to bad timing.

The conclusion isn’t quite true as of now, as it involves the new Iris replication system, which isn’t enabled (although compiled) by default, let alone that it’s still in experimental state.

9. Don’t Call Unnecessary RPCs

In the past tip we’ve already come across a couple of unnecessary RPCs, but the term was never established. Unnecessary RPC is an RPC that is in fact not needed, and could have been saved. For example, server-only logic does not necessarily have to be contained in a Server RPC if you can guarantee that a non-replicated function will only execute on the server.

Unnecessary RPCs in fact stem from the -sad yet true- fact of not being knowledgeable enough with the game framework multiplayer-wise.

  • Iterating upon a past example, here’s how a client could be notified locally of possession in Blueprint, without the need to use any needless RPCs:

Fix Unnecessary RPC

We filter by APawn::IsLocallyControlled() mainly cause we care about the owning client, which by nature exists at the local Controller level.

Note: For the listen-server player, the local controller exists on server, and for other client players, it exists on client.

10. Don’t Call Meaningless RPCs

One of the bad habits I keep seeing is the use of meaningless RPCs. With that I mean RPCs that don’t serve their true purpose, desirably executing on the other remote end, but instead they execute only locally, like in the following circumstances:

  • Server RPC invoked from the server.
  • Client RPC invoked from the server on a server-owned/unowned Actor.
  • NetMulticast RPC invoked from a client.
  • Client RPC invoked from a client.

Or even dropped:

  • Server RPC invoked from a client on a server-owned/unowned Actor.

It gets tricky and kinda hard to debug when you do a meaningless RPC without you knowing about it. We’ve already seen such example, which can occur due to bad timing of the RPC call.

11. Refrain from Overriding RPCs

I’m not very strict on this one, but overriding RPCs is grounds for being shot. In my humble opinion, keeping networking code abstracted from game-specific code helps with an essential daily coding habit: Debugging.

The simple other reason is consistency. You should keep in mind that you override MyFuncRPC_Implementation() in the child class, and not MyFuncRPC(), and in case you want to call a parent class implementation, then you call Super::MyFuncRPC_Implementation(), and not Super::MyFuncRPC() or you’ll get into an infinite loop:

MyFuncRPC()  MyFuncRPC_Implementation()  Super::MyFuncRPC()  MyFuncRPC_Implementation()  Super::MyFuncRPC()  ...

You might have understood the chain of calls by yourself now, but the way RPCs work, MyFuncRPC() internally calls MyFuncRPC_Implementation() as you could see in the chain of calls above. Anywhere you want to call the RPC, you always call the regular version without the _Implementation() for the RPC to be meaningful, otherwise the RPC gets called locally.

Lastly, you can’t override RPCs in Blueprint by adding BlueprintImplementableEvent or BlueprintNativeEvent to the RPC UFUNCTION() macro, as UHT (Unreal Header Tool) will prohibit you, not to mention that you can’t add those tags to a virtual function.

11.1 Override RPCs The Right Way

Here’s how you should “override” RPCs in C++:

MyExampleClass.h

/** This function has the RPC implementation, and is overridable from C++ */
UFUNCTION()
virtual void MyFunc();

/** This is the real RPC function by definition, and just calls MyFunc() internally */
UFUNCTION(Client, Unreliable)
void MyFuncRPC();

12. Differentiate Reliable from Unreliable RPCs

Any replicated event, can either be Reliable or Unreliable. By default, RPCs are unreliable.

12.1 Reliable RPCs

Reliable events are guaranteed to reach their destination (assuming ownership rules are followed) in the order they were sent and as long as they are in the same translation unit (being handled by the same ActorChannel), as they will be resent until they are acknowledged. However, they introduce more bandwidth, and potentially latency, to meet this guarantee. Try to avoid sending reliable events too often, like on tick for example, since the engine’s internal buffer of reliable events may overflow, and as a result the associated player will be disconnected!

Use Reliable RPCs for events that should not be missed, with those usually being player input that doesn’t happen on tick, and gameplay critical events that are better be late than missed, that could potentially affect the state of the game.

Warning: Never use a Reliable RPC on tick. Also, be cautious when binding Reliable RPCs to player input. Players can repeatedly press buttons very rapidly, and that will overflow the queue for Reliable RPCs. You should use some way of limiting how often players can activate these.

12.2 Unreliable RPCs

Unreliable events work as their name implies - they may not reach their destination, or reach it with gaps in the RPC calls (even though they will still be processed in order), in case of packet loss on the network, or if the engine determines there is a lot of higher-priority traffic it needs to send, meaning they won’t be resent if they’re lost. As a result, unreliable events use less bandwidth than reliable events, and they can be called safely more often.

Use Unreliable if the RPC is being called very often, such as on tick, or if you are calling a cosmetic event that isn’t critical to gameplay, such as spawning visual and sound effects.

13. Differentiate Replicated Properties from RPCs

Replicated properties and RPCs, are two quite powerful tools, that almost every Unreal multiplayer game inevitably takes advantage of.

13.1 Multiplayer Golden Rule

This is a super important rule, that diverges into three golden subrules of multiplayer, of which you should live by:

  1. Use replicated properties for replicating stateful events.
  2. Use Multicast/Client RPCs for replicating transient (not stateful) or cosmetic in nature events.
  3. Use Server RPCs to make the client communicate with the server (basically it’s the only way), and validate the passed data if needed.

13.2 Multiplayer Golden Rule Exceptions

Good to note, that the golden rule above is not set in stone, and there are always exceptions:

  1. Sometimes the use of RPCs would be quite good even for stateful replication, where for example the maximum bunch size (65535B=64KB) is exceeded, at which point chunking the data into reliable RPCs, while avoiding overflowing the reliable buffer size (RELIABLE_BUFFER that defaults to 256, which is the maximum number of reliable unacked packets plus the current outgoing bunches at any given time) could prove to be a useful pattern, although this must be done with extreme caution, or otherwise clients will start getting disconnected! Here’s to an astounding article that goes more in depth on how this can be done.

  2. Some other times, and by the same previous analogy, you can use property replication even for transient/lossy events. Here’s to a spectacular article that demonstrates that.

13.3 Comparison by Performance

Often times peeps compare replicated properties with RPCs based on performance, to consider which one to use. This comparison stems basically from the fact that they don’t really understand that they are different tools serving quite different purposes, making the comparison out of place for the most part.

Generally speaking, RPCs end up invoking a lot more virtual function calls and lookups than property replication, and these need to happen every time an RPC is invoked. Further, these calls will happen in the frame right where the RPC is called as opposed to batched at the end of the frame (like with property replication), so it’s more likely that you’ll have cache misses for one off RPCs.

Replicated properties actually use more memory, since server and clients store shadow states about every property to know when changes occur.

All properties and RPCs incur a minimum 2-byte property header to identify which property/RPC it is. Properties embedded in Structs and Subobjects will have larger headers. This is usually not something significant enough to change the design of the code however, apart from in very extreme circumstances.

For a more in depth comparison, check this article.

14. Validate Data where Needed

At this point you should have already understood that your only way to pass data from client to server in Unreal, is via Server RPC.

It’s not uncommon to see peeps pass data indifferently from client to server, trusting the client without validating its data. Hence why we end up with the following scenarios in many games:

  • Cheaters having infinite health
  • Cheaters having infinite currency
  • Cheaters shooting through walls

And the list just grows…

What you ought to be doing instead is to use a technique most games nowadays use, that follows the principle of “trust and verify”, in which we trust the client about the data it passes to the server (for responsiveness), and then we validate it on the server. Validation on the server can be done with the help of:

  • RPC validation function: Usually it does nothing but return true. The reason for this, is you don’t want to kick a player simply because there was a client-server disagreement, as such can happen frequently in any real world networked game scenario.
  • Server-driven data
  • External backend with database setup

Here to a funny vulnerability story Shooter Game has, that spread to many games.

15. Know the Replication Guarantees

It’s important to understand what is guaranteed and what isn’t when it comes to property and RPC replication and what steps should be taken to account for this in your game code. This tip builds upon Epic’s article.

15.1 Replication Reliability Guarantees

15.1.1 Property Replication Reliability Guarantees

Property replication is guaranteed to be reliable. This means that the property of the client version of the Actor will eventually reflect the value on the server, but the client will not necessarily receive notification of every individual change that happens to a property on the server. The is because the Actor does not necessarily replicate every frame and packets may be dropped, meaning that every update made on the server won’t necessarily be received by the client.

If you need frequent changes to be received by the client, you may want to increase the NetUpdateFrequency of the Actor to ensure it replicates each frame. However, even a high update frequency doesn’t guarantee that every change will be received, as packet loss can result in a change being dropped. How you handle this will depend on the specifics of your game, but in many cases, smoothing between received values can help mitigate issues with updates not being received.

15.1.2 RPC Reliability Guarantees

Reliable RPCs sent are almost always guaranteed to be received and processed, as they will be resent until they are acknowledged. If they never reach their target clients, which rarely happens (massive packet loss for several seconds), then the affected clients are kicked. On the other hand, Unreliable RPCs won’t be resent if they’re lost, and neither will be the instigator for disconnecting the client.

However, RPCs don’t count for clients that haven’t joined yet, meaning they won’t get resent to a client if the client wasn’t there when the RPC was initially sent. That’s why at the end of the day property replication is considered more reliable over a wider spectrum of scenarios, with the most famous being syncing game state data for late-joiners, also referred to as JIP (join in progress) clients.

15.2 Replication Timing and Ordering Guarantees

15.2.1 Property Replication Timing Guarantees

As we mentioned before, each Actor has its own NetUpdateFrequency, which dictates how often it will be considered for replication.

Once an Actor is considered for replication on a frame, all trivial properties (anciently referred to as POD types) that should replicate will be sent at once, and the client should receive and apply these changes within a single frame. On the other hand, as you would expect, unmapped properties (un-hardcoded FNames and UObject*) are excluded from such guarantee. They are called unmapped because their NetworkGUID, which is a globally unique identifier for network related use, that gets serialized alongside the Object to be replicated, has not been mapped to it just yet, such that the Object has the value of null, but once the Object replicates (loads on client), and the NetworkGUID gets resolved into it, then they will become mapped properties. To be able to know when a NetworkGUID gets resolved into a replicated Object, we use replication callback functions known as OnReps:

MyObject.h

/** Replication callback to when MyObject becomes mapped/valid on client */
UFUNCTION() // It must be marked as UFUNCTION so it's found by name
void OnRep_MyReplicatedObject();

/** Pointer to the replicated object. Note this is just replicating a reference to the object (i.e. the object itself must be marked/made to replicate in the first place) */
UPROPERTY(ReplicatedUsing=OnRep_MyReplicatedObject)
UObject* MyReplicatedObject;

Note: The intention above is anything that derives from UObject, so the same applies for the common use of AActor*.

Practically speaking, it takes less time to replicate a POD than to replicate an Object. When spawning a replicated Actor (not placed in map) on the server, all its replicated POD properties, as long as they are set in the same frame the Actor is spawned in (i.e. no longer than BeginPlay getting called), are guaranteed to have been replicated to the client by the time BeginPlay is called client-side for example. The reason for this, is that on client, BeginPlay of replicated Actors that are spawned from replication (not loaded from map) will always be called after PostNetInit is called:

Actor.h

/** Always called immediately after spawning and reading in replicated properties */
virtual void PostNetInit();

Actor.cpp

void AActor::PostNetInit()
{
    if(RemoteRole != ROLE_Authority)
    {
        UE_LOG(LogActor, Warning, TEXT("AActor::PostNetInit %s Remoterole: %d"), *GetName(), (int)RemoteRole);
    }

    if (!HasActorBegunPlay())
    {
        const UWorld* MyWorld = GetWorld();
        if (MyWorld && MyWorld->HasBegunPlay())
        {
            SCOPE_CYCLE_COUNTER(STAT_ActorBeginPlay);
            DispatchBeginPlay();
        }
    }
}

Note: If World hasn’t begun play on client by the time PostNetInit is called, then the replicated GameState is what dispatches BeginPlay. Placed in map Actors won’t have PostNetInit called for them, and will have BeginPlay dispatched from the replicated GameState as well.

However, again Names that are not hardcoded in UnrealNames.inl and pointers to other Objects (especially dynamic ones - spawned at runtime) are not guaranteed to be valid at such point, and should ideally rely upon OnRep replication callbacks to determine when they replicate.

For changes made to multiple properties across multiple frames, generally these changes should be received in the order that they were made. However, unideal network conditions can result in these changes being dropped and resent, so they may be received later. For example:

The server has three replicated booleans that are changed over three net updates respectively. The server’s states look like this for the three properties:

0,0,0 → 1,0,0 (the first property’s change is sent) → 1,1,0 (the second property’s change is sent) → 1,1,1 (the third property’s change is sent).

After the third change is sent, the server detects the second change was lost, so it is resent. The client receives the first change, but because the second change is dropped and resent, the third change is received before the second. The client’s states look will like this: 0,0,0 → 1,0,0 → 1,0,1 → 1,1,1.

Again, the guarantee that the state on the client will eventually reflect the state on the server is upheld, but until then, the client’s properties can be in a state (1,0,1) that never existed on the server. This is because Objects or even Structs don’t replicate atomically (all at once), meaning only properties that changed from the shadow state of the Actor (used to compare if the property is different and therefore needs to be replicated) will be sent. I don’t believe Objects can support atomic replication natively, but Structs definitely can.

While we’re at it, it’s good to mention that dynamically (at runtime) attaching an ActorComponent to a replicated just spawned Actor server-side, is guaranteed to arrive in the same initial packet the Actor is sent so long as you do it in the same frame you spawn it (no longer than AActor::BeginPlay() getting called).

At this point you should understand that the term frame is so important in this context, for the simple reason that Actors and the properties they entail are potentially gathered for replication at the end of every frame.

15.2.2 GameState Replication Timing Guarantee

GameState is guaranteed to be valid when any Actor on client calls BeginPlay.

Let’s look at the following code to understand how such a guarantee is legit:

GameModeBase.h

/** Transitions to calls BeginPlay on actors. */
UFUNCTION(BlueprintCallable, Category=Game)
virtual void StartPlay();

GameModeBase.cpp

void AGameModeBase::StartPlay()
{
    GameState->HandleBeginPlay();
}

GameStateBase.h

/** Called by game mode to set the started play bool */
virtual void HandleBeginPlay();

/** By default calls BeginPlay and StartMatch */
UFUNCTION()
virtual void OnRep_ReplicatedHasBegunPlay();

/** Replicated when GameModeBase::StartPlay has been called so the client will also start play */
UPROPERTY(Transient, ReplicatedUsing = OnRep_ReplicatedHasBegunPlay)
bool bReplicatedHasBegunPlay;

GameStateBase.cpp

void AGameStateBase::HandleBeginPlay()
{
    bReplicatedHasBegunPlay = true;

    GetWorldSettings()->NotifyBeginPlay();
    GetWorldSettings()->NotifyMatchStarted();
}

void AGameStateBase::OnRep_ReplicatedHasBegunPlay()
{
    if (bReplicatedHasBegunPlay && GetLocalRole() != ROLE_Authority)
    {
        GetWorldSettings()->NotifyBeginPlay();
        GetWorldSettings()->NotifyMatchStarted();
    }
}

WorldSettings.h

/**
 * Called from GameStateBase, calls BeginPlay on all actors
 */
virtual void NotifyBeginPlay();

WorldSettings.cpp

void AWorldSettings::NotifyBeginPlay()
{
    UWorld* World = GetWorld();
    if (!World->bBegunPlay)
    {
        for (FActorIterator It(World); It; ++It)
        {
            SCOPE_CYCLE_COUNTER(STAT_ActorBeginPlay);
            const bool bFromLevelLoad = true;
            It->DispatchBeginPlay(bFromLevelLoad);
        }

        World->bBegunPlay = true;
    }
}

Now if your’e familiar with the traditional game flow, and have read the code above, you’ll understand that first of all UWorld::BeginPlay() is called on the server, calling AGameModeBase::StartPlay(), which calls AGameStateBase::HandleBeginPlay(), which in return sets AGameStateBase.bReplicatedHasBegunPlay (that is marked for replication) to true, and also calls AWorldSettings::NotifyBeginPlay(), which calls BeginPlay on all Actors available on the server at this point. When the previously set boolean replicates, its replication callback GameStateBase::OnRep_ReplicatedHasBegunPlay() gets automatically called, which calls AWorldSettings::NotifyBeginPlay(), that calls BeginPlay on all Actors available on client at that point. bReplicatedHasBegunPlay replicating simply means that the GameState that entails it replicated as well. This guarantees us having a valid GameState by the time BeginPlay is called for any Actor existing on client at this point. Any Actors that may exist at a later point on client, will have their BeginPlay called either from:

  1. AActor::PostNetInit(): These are dynamic Actors spawned implicitly from replication on client, usually at a point later than the initial phase of the game, or having a NetUpdateFrequency lower than the one the GameState has (PlayerState for example).
  2. AActor::PostActorConstruction(): These are Actors spawned explicitly (not from replication) on client, usually at a point later than the initial phase of the game.

In order for BeginPlay to get called in both scenarios, the World must have bBegunPlay on client, which must have already happened at this point, and which guarantees us a valid GameState on client.

With all that being said, this leads us to our exceptional guarantee:

GameState is guaranteed to be valid when any Actor on client calls BeginPlay.

15.2.3 Property Replication Ordering Guarantees

There is no guarantee made regarding the order in which properties will be received or the order in which OnReps will be called. Any order is considered an implementation detail that should not be relied upon. If the order of an Actor’s property replication is important to your game, you may need to implement OnReps to track which properties have been updated on a frame. After the replicated values have been received and their OnReps called, you can handle the changes in the PostRepNotifies function. You may also need to save certain received values in their respective OnReps until they’re ready to be used.

It’s also important to know that the ordering of replication between Actors is not guaranteed. If two Actors have properties changed on the same frame, they may not be replicated or received on the same frame for a number of factors, such as differing update frequencies, relevancy/priority differences, or packet loss. If you have one Actor that is dependent on another for replication, such as an inventory Actor depending on a player Character, using OnReps to track state and determine when an Actor is available, is again a useful strategy for handling these kinds of situations. The Replication Graph also provides support for building and handling these dependencies as well.

15.2.4 RPC Ordering Guarantees

Any reliable RPCs sent are guaranteed to be received and processed in the order they were sent, as they will be resent until they are acknowledged. However, regardless of reliability, order is still not guaranteed between different Actors, and there is no guarantee that RPCs called on the same frame will arrive on the same frame.

In an ideal world, with optimal network conditions, unreliable RPCs are not any different, however once packet loss comes into the picture, gaps start to arise in the unreliable bunches, as they won’t be resent if they’re lost.

16. Use Atomic Replication

Atomic replication of an object means replicating all properties within that object in the same packet, regardless of whether they changed or not, every time an object replicates. By default, Objects and Structs don’t replicate atomically, meaning only what changes gets sent, so they delta net-serialize.

Note: Objects or Structs will atomically replicate (send all at once) all properties that are changed in the same frame as was mentioned before, however they don’t replicate atomically per se, so watch out for the context in which “atomically” is said!

Atomic replication is crucial in situations where the client must never end up in a situation that never existed server-side potentially leading to an unexpected behavior. Such situations happen when you replicate multiple properties that relate to each other, and that you change them over multiple different frames, so some changes in between won’t get sent right away to the client because of packet loss, leading to a state client-side that never existed server-side, causing unexpected bad behaviors.

To solve this matter, we pack such properties that we need them to replicate “always together” in a Struct, and then we implement NetSerialize function for it (which we could also use to compress its properties values smaller as forms of bandwidth/CPU optimization), by serializing these properties in the archive, such that they can be always deserialized on client back into the state in which they were serialized on server. Here can be found a practical example. Note however that some of these properties you pack together, can potentially not be valid by the time you receive your packet (if they were references to an Object for example) as it was said previously.

Warning: Using atomic replication out of place, can be of a burden on bandwidth, considering all properties are always sent for replication.

While native Structs like FLinearColor doesn’t implement NetSerialize function, others do implement it, such that you can take as an example: FVector (you will find as TVector though), FRepMovement, and FHitResult.

Sometimes you might want to go a step further and ensure that a whole Object replicates atomically for reasons. The reason I had to do this at work recently was that I needed all properties of an Object to be valid all at the same time for an atomic operation (an operation that is done once, and needs all these properties to be valid when it gets executed). Again, this is not something you want to do regularly, but only when necessary.

In my case the Object defines customization settings for the Character. The code I ended up writing roughly looks like this:

MyCharacterComponent.h

// Apply all customization settings to our character in a single atomic operation
void ApplyCustomizationSettings();

// Serializes applied customization settings on the server, so they are deserialized on clients
void SerializeCustomizationSettings();

// Deserializes customization settings on clients, so they are applied for them
void DeserializeCustomizationSettings(const TArray<uint8>& customizationSettingsData);

/** Replication callback for the serialized customization settings byte data */
UFUNCTION()
void OnRep_CustomizationSettingsData();

// Server serialized customization settings byte data to be applied to client instances in an atomic operation
UPROPERTY(ReplicatedUsing=OnRep_CustomizationSettingsData)
TArray<uint8> CustomizationSettingsData;

MyCharacterComponent.cpp

void UMyCharacterComponent::ApplyCustomizationSettings()
{
    // Customization settings are applied here

    SerializeCustomizationSettings();
}

void UMyCharacterComponent::SerializeCustomizationSettings()
{
    if (GetOwnerRole() == ROLE_Authority && GetCustomizationSettings())
    {
        // Serialize customization settings on server
        TArray<uint8> customizationSettingsData;
        FMemoryWriter memoryWriter(customizationSettingsData);
        FObjectAndNameAsStringProxyArchive Ar(memoryWriter, false);
        CustomizationSettings->Serialize(Ar);

        CustomizationSettingsData = customizationSettingsData;
    }
}

void UMyCharacterComponent::OnRep_CustomizationSettingsData()
{
    DeserializeCustomizationSettings(CustomizationSettingsData);
}

void UMyCharacterComponent::DeserializeCustomizationSettings(const TArray<uint8>& customizationSettingsData)
{
    if (customizationSettingsData.IsEmpty())
    {
        return;
    }

    // Deserialize customization settings on client
    UCustomizationSettings* customizationSettingsData = GetCustomizationSettings();
    FMemoryReader memoryReader(customizationSettingsData);
    FObjectAndNameAsStringProxyArchive Ar(memoryReader, true);
    customizationSettingsData->Serialize(Ar);

    ApplyCustomizationSettings();
}

With all that being said, you should be really careful to when you do something similar. In my case, I needed to do this only when a client joins a game session, and under certain conditions that I can’t disclose, such that it doesn’t happen frequently for it to consume much bandwidth. It’s good to note that this method of custom network serialization has some limitations, but it goes out of the article’s scope. Lastly, unless you got the reasons, you should always consider something else, but in my case this was a solid consideration that thankfully my lead guided me to.

17. Avoid GameMode Bad Habits

When running the Server-Client model, the GameMode will only exist on the server. Here are the wrong habits that involve this Actor:

  • Trying to retrieve it on client and getting null. Instead, you only retrieve it on the server.
  • Filtering client-server execution paths, when it has only one execution server path, making the filtering meaningless.
  • Calling RPCs on it, which will end up being meaningless, such that they will run locally on the server, considering GameMode is not a replicated Actor, and in fact exist only on the server.

18. Beware of NetMulticast RPC Myth

Currently, almost all sources of Unreal Engine multiplayer knowledge will tell you the following info about Multicast RPCs:

Multicast RPCs are called on the server and are executed on the server and all connected clients.

Anyone who reads this piece of info will be attracted by the myth that Multicast RPCs that are called on a replicated Actor on the server, will be executed on all of its instances (its server instance, and all its instances on clients), with no respect to relevancy for example, and that is totally wrong.

This, along with that they are not aware of the golden rule of multiplayer will lead them to use this type of RPCs out of place.

The correct piece of info though is:

Multicast RPCs are called on the server and are executed on the server and all connected clients that the Actor is relevant to.

You have to be aware that Multicast RPCs won’t account for either of the following:

  1. Join-in-progress clients: Late joiners won’t have the Actor in question spawned in the first place for the RPC to get executed on it.
  2. Clients the Actor isn’t relevant for: Imagine a game scenario where two players, A and B, are far away from one another, such that the distance between them is bigger than net cull distance (square root of AActor.NetCullDistanceSquared). Calling a Multicast RPC on A’s Pawn server-side, will execute the RPC on the A’s Pawn on the server, and on client where A’s Pawn is locally controlled (has a LocalRole of ROLE_AutonomousProxy). However, the RPC won’t get executed on A’s Pawn on the instance where B’s Pawn is locally controlled (A’s Pawn won’t be locally controlled and will have a LocalRole of Role_SimulatedProxy).
  3. Replay recording/scrubbing: This can be used to implement Killcam for example.

To handle these scenarios, you will need to do stateful replication.

19. Properly Replicate SkeletalMesh Assignment

One of the very common problems that beginners face in multiplayer is: “How do you replicate the assignment/change of a SkeletalMesh to other clients?”

Clearly neither the SkeletalMeshComponent (inherits from SkinnedMeshComponent) nor its SkeletalMesh replicate:

SkinnedMeshComponent.h

/** The skeletal mesh used by this component. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Mesh", meta = (DisallowedClasses = "DestructibleMesh"))
TObjectPtr<class USkeletalMesh> SkeletalMesh;

What beginners often do to solve this problem is one of the following ultimately failing tries:

  1. Replicate the SkeletalMeshComponent: Which won’t do anything good, considering the internal SkeletalMesh isn’t replicated. This will also potentially break the CharacterMovementComponent networking.
  2. Multicast the change: This is clearly bad because it is not stateful, and will break in many scenarios.

What you should do instead is replicate the “action” or the “state” that drives the change. That can as simple as a replicated boolean variable bSkeletalMeshAssigned that is tied to a replication callback OnRep_SkeletalMeshAssigned(), that assigns the SkeletalMesh to your Character. Instead of the boolean, you could have even replicated a pointer Mesh variable that points to the asset itself:

MyCharacter.h

UPROPERTY(ReplicatedUsing=OnRep_Mesh)
USkeletalMesh* Mesh;

UFUNCTION()
void OnRep_Mesh();

MyCharacter.cpp

void AMyCharacter::SetMesh(USkeletalMesh* NewMesh)
{
    Mesh = NewMesh;

    // This is essential in case you want to support listen-servers.
    // As we noted earlier OnReps don't get called automatically for servers like they do for clients,
    // hence why we call it manually. This is also considered a good practice (calling OnReps in Setters of replicated properties,
    // so you don't need to call it everywhere in code the property is set, and possibly even to forget to do so)
    // This also helps in the case of prediction, where you can set the property on client and in which case the OnRep won't get called automatically
    OnRep_Mesh();
}

void AMyCharacter::OnRep_Mesh()
{
    GetMesh()->SetSkeletalMesh(Mesh);
}

Here you can find a similar problem that demonstrates the misuse of Multicast RPC, that then solves the problem properly like we did here above.

20. Workaround Native Replicated Property Missing a Replication Callback

Recently I was faced with the following issue at work:

There is this native replicated property that I’m using and it’s amazing, however suddenly I want to know when it replicates in a nice way, but sadly it’s Replicated and not ReplicatedUsing!

For you to be able to know when a property replicates in a clean optimal way, you should have that property marked as UPROPERTY(ReplicatedUsing=OnRep_MyProperty), which isn’t always the case for native properties already provided to you by the engine, and in my case that was:

PlayerState.h

/** Whether this player is currently a spectator */
UE_DEPRECATED(4.25, "This member will be made private. Use IsSpectator or SetIsSpectator instead.")
UPROPERTY(Replicated, BlueprintReadOnly, Category=PlayerState)
uint8 bIsSpectator:1;

That variable was being set to true when we initiate spectating, however we wanted to be able to “listen” to when it replicates so we could do something in response. Some of you might wonder why I didn’t simply use the native function APlayerController::BeginSpectatingState(), and the reason is that we want to avoid calling APlayerController::ChangeState(), which will make the PlayerController unpossess the Pawn and create a situation where it can no longer execute gameplay abilities, which can break one of our exclusive game features.

At this point I was suggested by my smart lead to do the following trick:

MyPlayerState.h

/** Always called immediately after properties are received from the remote. */
virtual void PostNetReceive() override;

// Whether our player was a spectator in the previous net update
// Used as a gate - we close the gate after we broadcast SpectatingStartDelegate, and reopen it when we broadcast SpectatingEndDelegate
bool bStartedSpectating = false;

// Delegate for when the client goes into/out-of spectator mode, and bIsSpectator replicates to it
FShouldBeSpectatingDelegate ShouldBeSpectatingDelegate;

MyPlayerState.cpp

void AMyPlayerState::PostNetReceive()
{
    Super::PostNetReceive();

    // This is a hack replication callback to check if bIsSpectator has replicated to us, and is different than our local value
    // This way we can reliably dictate when the client goes into spectator mode

    if (IsSpectator())
    {
        if (!bStartedSpectating)
        {
            bStartedSpectating = true;
            ShouldBeSpectatingDelegate.Broadcast(bStartedSpectating);
        }
    }
    else
    {
        if (bStartedSpectating)
        {
            bStartedSpectating = false;
            ShouldBeSpectatingDelegate.Broadcast(bStartedSpectating);
        }
    }
}

To explain, PostNetReceive is a native function I used as a form of a “global replication callback”, along with implementing a gate inside, so we decide if the potentially new value received is really new, so we broadcast the proper delegate in response. As you can tell, it’s way more convoluted than if there was a replication callback that we could utilize out of the box, but it works nicely.

21. Know when Objects are Network Referenceable

Peeps usually tend to think that in order to be able to reference an Object over the network, it must be replicated, and that’s not entirely correct. This is because they are not aware that a stably named Object is a thing, which we will go over shortly.

Generally, Object references are handled automatically in Unreal’s multiplayer framework. What this means is, if you have an Object property that is replicated, the reference to that Object will be sent over the network as a special ID that is assigned by the server. This special ID is a NetworkGUID. The server is in charge of assigning this ID, and then notifying all connected clients of this assignment.

Here’s how you replicate an Object reference:

MyOtherObject.cpp

UPROPERTY(Replicated)
UMyObject* MyObject;

In this case, MyObject property will be a replicated reference to the Object that this property refers to.

For an Object to be legally referenced over the network, it must be supported for networking. To check for this, you can call UObject::IsSupportedForNetworking(). This is generally considered a low level function, so it’s not common to need to check this in game code.

In general, these are the Objects that are network referenceable:

  • Any replicated Object: These can be Actors, or Subobjects (ActorComponents and UObjects).
  • Any stably named Object: These are generally non-replicated Actors, or Subobjects.

21.1 Stably Named Objects

Stably named Objects are simply Objects that exist on both server and client, and have the exact same name. Initially they are referred to over the network by their name, until it’s replaced by the assigned NetworkGUID.

Actors are stably named if they were:

  • Loaded directly from packages
    • Actors placed in maps.
  • Act like loaded directly from packages
    • These are dynamically spawned Actors that fool the engine into thinking they are net startup Actors placed in maps (assigned static NetworkGUIDs so they are not destroyed on clients when they are not relevant to them). That’s done by setting AActor.bNetStartupActor and AActor.bNetLoadOnClient to true, and them being deterministically named (same name on server and client).
  • Marked to be addressable by name (via AActor::SetNetAddressable())
    • These are dynamically deferred spawned Actors, that are marked to be net addressable before AActor::FinishSpawning() is called, and are deterministically named (same name on server and client).

Note: Epic is quite strict on AActor::SetNetAddressable() being called before AActor::FinishSpawning() is called. However, from my own experience, it can be called at any point in time.


ActorComponents are stably named if they were:

  • Default Subobjects
    • Created in C++ constructor, so they part of the CDO - Class Default Object.
  • Loaded directly from packages
    • Added to Actors placed in maps.
  • Marked to be addressable by name (via UActorComponent::SetNetAddressable())
    • Automatically (done by native code)
      • Added via SCS - SimpleConstructionScript (created from a template defined in the Components section of the Blueprint. They are not part of the CDO, but of the SCS).
      • TimelineComponents (are behind the scenes added via UCS - UserConstructionScript).
    • Manually marked to be addressable by name (done by you)
      • Deterministically named (same name on server and client). These are generally dynamically spawned, but could be for example a SplineMeshComponent added via UCS.


All other UObjects (non-Actors and non-ActorComponents) are stably named if they were:

  • Default Subobjects
    • Created in C++ constructor, so they part of the CDO.
  • Loaded directly from packages.
  • Deterministically named (same name on server and client), with UObject::IsNameStableForNetworking() returning true. These are generally dynamically spawned.

Note: Whether Objects were replicated or stably named, ultimately they will be referred to by their NetworkGUID, however when stably named, their name will be sent only once, and then replaced by NetworkGUID.

Beware: Marking a replicated Actor to be addressable by name before it’s actually replicated, won’t make it replicate, and it will only exist on the server!

22. Coming soon:tm:

Coming soon:tm:

23. Be Aware of Objects Prerequisites to Run RPCs

I’ve decided to put this into a tip, because I’ve seen too many misinformation out there, both official and non-official.

Actors are potentially able to run RPCs if there were replicated.


ActorComponents are potentially able to run RPCs if there were:

  • Created from replication (spawned server-side and replicated), and their owning/outer Actor is replicated.
  • Stably named (Loaded/Spawned on both ends (server/client) with the same name relative to their owning.outer Actor), and their owning/outer Actor is replicated.
  • Neither replicated, nor stably named, and the RPC is a reliable Multicast/Client. This will however, spawn an ActorComponent on client, and link it with its server version by the NetworkGUID assigned, however the engine doesn’t seem to quite like it and you will end up crashing (I have to yet find a way not to crash).


All other UObjects (non-Actors or non-ActorComponents) are potentially able to run RPCs if they had the relevant code, their Outer Actor replicated, and they were:

  • Created from replication (spawned server-side and replicated), and their Outer Actor replicated.
  • Fully stably named (Loaded/Spawned on both ends (server/client) with the same full path name, so both their name and their Outer Object name are the same - see UObject::IsFullNameStableForNetworking() for reference), and their owning Actor replicated. I found that you can bypass stably naming the Outer if it was replicated, and then simply override UObject::IsFullNameStableForNetworking() to return true.
  • Neither replicated, nor stably named, and the RPC is a reliable Multicast/Client. This will however, spawn a UObject on client, and link it with its server version by the NetworkGUID assigned, however the engine doesn’t seem to quite like it and you will end up crashing (I have to yet find a way not to crash).

24. Stick to an RPCs Naming Convention

When it comes to naming your different type of RPCs, a very good practice and also a convention, is to prepend the name of your RPC function based on its type:

  • Client RPC: ClientMyFunctionRPC()
  • Server RPC: ServerMyFunctionRPC()
  • NetMulticast RPC: MulticastMyFunctionRPC()

This is very useful to determine at a glance what machines this function will be potentially executed on during a multiplayer session.

25. Know why RPCs have No Return Values

RPCs are one way calls, that’s why they simply have no return values.

If you want to simulate a return value, you will have to ping-pong your RPCs. Meaning that an RPC gets sent one way, pulls some data, passes it as an RPC parameter to another RPC that is sent back the other way around.

26. Avoid Using the Level Blueprint

Level Blueprints aren’t Actors that you use often in single-player, and so neither in multiplayer. However they’re great for one-off prototypes, and getting familiar with the Blueprints system, but they are specific to the level that they’re used in. This does mean that Level Blueprints can be great places to set up functionality specific to the level, or Actors in it. Some examples would be kicking off a cinematic when a certain trigger is touched, or opening a particular door after you kill all the enemies.

Let’s look at the following piece of code that I see from time to time in the Level Blueprint:

Level Blueprint

And in return an error is generated that goes along these lines:

  
PIE: Error: Blueprint Runtime Error: "Accessed None trying to read property CallFunc_GetPlayerController_ReturnValue".

This is a code that can take many forms but has one big caveat: Timings are not working to your side when it comes to the Level Blueprint!

PlayerController can potentially get spawned after the LevelScriptActor (another way of saying Level Blueprint, as it’s its native parent class), meaning that it’s not there by the time BeginPlay is called, resulting in the error we’ve seen above, and with the ViewTarget being the possessed Pawn in case one exists.

In addition, keep in mind that this code also uses the ominous GetPlayerController(0)!

27. Be Aware of Placed in Map Actors’ Subtleties

Placed-in-map (static) replicated Actors will quite surprise you in how different they can operate in a networked game from runtime-spawned (dynamic) replicated Actors:

  • They don’t get destroyed when net culled, meaning that when a placed in map Actor goes out of the relevancy range for a connection, it doesn’t get destroyed for that connection.
  • They don’t get AActor::PostNetInit() called on them.
  • They are loaded from the map rather than spawned from replication.
  • Their replication callbacks, also known as OnRep functions, are not necessarily called before BeginPlay, like it’s guaranteed for POD types in dynamic replicated Actors, and will even usually have them called after BeginPlay is called.

28. Utilize Network Debugging Tools

Coming Soon:tm:

28.1 Console Commands for Network Debugging

Coming Soon:tm:

Updated: