In this post we’re going to re-create the Blueprint project from the official Networking Tutorials of Unreal Engine 4 into a C++ project. I highly advise you to follow the official tutorials first, since they explain in great detail some of the concepts that will be briefly mentioned here. This post assumes you’re familiar with the basic networking terminology and functionality inside UE4 (explained in videos 1 – 4 in the official tutorials).

Before we start re-creating the mentioned project in C++, here is the end result of this post:

In the first part, we’re going to create some basic networking functionality, meaning, we will create a mechanism that decreases the health and bomb count by a press of button. In the next part, we’re going to modify this functionality a bit. Specifically, we will spawn bombs and when they explode we will decrease the HP in every affected character.

A link with the full source code of networking tutorials will be given in the final part.

In the above video, both characters have a text renderer component that displays their Health and Bomb Count which are replicated variables. This means that we want every character to be able to “see” the Health and Bomb count of other characters.

Since damage is an important part of our gameplay, meaning we don’t want any cheating to take place and ruin our fun, we’re going to tell the server to apply any damage to specific players. This means that in case a character wants to damage another character, the following logc we’ll be executed:

The character will tell the server that he wants to damage another character

The server will perform a check to determine if this is a valid request (for example a dead character can’t damage another character). In case of a valid request, the server will apply the damage to the requested character



Having said that, let’s start by replicating the Health and Bomb variables. Then, we will create the mentioned damage logic.

Create a Third Person C++ template project and add the following include to the header file of your project [YourProjectName.h]:

#include "Net/UnrealNetwork.h"

This is necessary in order to use any networking features in our game.

Replicating Health and Bomb Count

In order to replicate these variables, we’re going to use RepNotifies. RepNotifies execute a certain function when a variable changes. Each variable has its own function. For example, when the Health variable changes, a function named OnRep_Health will get called. The naming of the function follows the official naming convention of UE4.

Before adding any code, add the following header before the .generated.h file inside your character’s class:

#include "Components/TextRenderComponent.h"

We need that in order to use the TextRenderComponent that is displayed in the video.

Then, add the following code inside the header file of your character’s class:

Header file part1 protected: /** The health of the character */ UPROPERTY(VisibleAnywhere, Transient, ReplicatedUsing = OnRep_Health, Category = Stats) float Health; /** The max health of the character */ UPROPERTY(EditAnywhere, Category = Stats) float MaxHealth = 100.f; /** The number of bombs that the character carries */ UPROPERTY(VisibleAnywhere, Transient, ReplicatedUsing = OnRep_BombCount, Category = Stats) int32 BombCount; /** The max number of bombs that a character can have */ UPROPERTY(EditAnywhere, Category = Stats) int32 MaxBombCount = 3; /** Text render component - used instead of UMG, to keep the tutorial short */ UPROPERTY(VisibleAnywhere) UTextRenderComponent* CharText; private: /** Called when the Health variable gets updated */ UFUNCTION() void OnRep_Health(); /** Called when the BombCount variable gets updated */ UFUNCTION() void OnRep_BombCount(); /** Initializes health */ void InitHealth(); /** Initializes bomb count */ void InitBombCount(); /** Updates the character's text to match with the updated stats */ void UpdateCharText(); public: /** Marks the properties we wish to replicate */ virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; virtual void BeginPlay() override; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 protected : /** The health of the character */ UPROPERTY ( VisibleAnywhere , Transient , ReplicatedUsing = OnRep_Health , Category = Stats ) float Health ; /** The max health of the character */ UPROPERTY ( EditAnywhere , Category = Stats ) float MaxHealth = 100.f ; /** The number of bombs that the character carries */ UPROPERTY ( VisibleAnywhere , Transient , ReplicatedUsing = OnRep_BombCount , Category = Stats ) int32 BombCount ; /** The max number of bombs that a character can have */ UPROPERTY ( EditAnywhere , Category = Stats ) int32 MaxBombCount = 3 ; /** Text render component - used instead of UMG, to keep the tutorial short */ UPROPERTY ( VisibleAnywhere ) UTextRenderComponent * CharText ; private : /** Called when the Health variable gets updated */ UFUNCTION ( ) void OnRep_Health ( ) ; /** Called when the BombCount variable gets updated */ UFUNCTION ( ) void OnRep_BombCount ( ) ; /** Initializes health */ void InitHealth ( ) ; /** Initializes bomb count */ void InitBombCount ( ) ; /** Updates the character's text to match with the updated stats */ void UpdateCharText ( ) ; public : /** Marks the properties we wish to replicate */ virtual void GetLifetimeReplicatedProps ( TArray < FLifetimeProperty > & OutLifetimeProps ) const override ; virtual void BeginPlay ( ) override ;

Then, inside the constructor of your class, create and attach the CharText:

CharText inside constructor //Init. text render comp CharText = CreateDefaultSubobject<UTextRenderComponent>(FName("CharText")); //Set a relative location CharText->SetRelativeLocation(FVector(0, 0, 100)); //Attach it to root comp CharText->SetupAttachment(GetRootComponent()); 1 2 3 4 5 6 7 8 //Init. text render comp CharText = CreateDefaultSubobject < UTextRenderComponent > ( FName ( "CharText" ) ) ; //Set a relative location CharText -> SetRelativeLocation ( FVector ( 0 , 0 , 100 ) ) ; //Attach it to root comp CharText -> SetupAttachment ( GetRootComponent ( ) ) ;

Moreover, add the following logic inside the GetLifetimeReplicatedProps:

GetLifetimeReplicatedProps void ANetworkingTutCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); //Tell the engine to call the OnRep_Health and OnRep_BombCount each time //a variable changes DOREPLIFETIME(ANetworkingTutCharacter, Health); DOREPLIFETIME(ANetworkingTutCharacter, BombCount); } 1 2 3 4 5 6 7 8 9 void ANetworkingTutCharacter :: GetLifetimeReplicatedProps ( TArray < FLifetimeProperty > & OutLifetimeProps ) const { Super :: GetLifetimeReplicatedProps ( OutLifetimeProps ) ; //Tell the engine to call the OnRep_Health and OnRep_BombCount each time //a variable changes DOREPLIFETIME ( ANetworkingTutCharacter , Health ) ; DOREPLIFETIME ( ANetworkingTutCharacter , BombCount ) ; }

Then, add the following logic for the DoRep functions as well as the initializations:

OnRep functions and initializations void ANetworkingTutCharacter::OnRep_Health() { UpdateCharText(); } void ANetworkingTutCharacter::OnRep_BombCount() { UpdateCharText(); } void ANetworkingTutCharacter::InitHealth() { Health = MaxHealth; UpdateCharText(); } void ANetworkingTutCharacter::InitBombCount() { BombCount = MaxBombCount; UpdateCharText(); } void ANetworkingTutCharacter::BeginPlay() { Super::BeginPlay(); InitHealth(); InitBombCount(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void ANetworkingTutCharacter :: OnRep_Health ( ) { UpdateCharText ( ) ; } void ANetworkingTutCharacter :: OnRep_BombCount ( ) { UpdateCharText ( ) ; } void ANetworkingTutCharacter :: InitHealth ( ) { Health = MaxHealth ; UpdateCharText ( ) ; } void ANetworkingTutCharacter :: InitBombCount ( ) { BombCount = MaxBombCount ; UpdateCharText ( ) ; } void ANetworkingTutCharacter :: BeginPlay ( ) { Super :: BeginPlay ( ) ; InitHealth ( ) ; InitBombCount ( ) ; }

As you can see, the On_Rep functions simply update the TextRenderComponent. Here is the logic of the UpdateCharText:

UpdateCharText void ANetworkingTutCharacter::UpdateCharText() { //Create a string that will display the health and bomb count values FString NewText = FString("Health: ") + FString::SanitizeFloat(Health) + FString(" Bomb Count: ") + FString::FromInt(BombCount); //Set the created string to the text render comp CharText->SetText(FText::FromString(NewText)); } 1 2 3 4 5 6 7 8 void ANetworkingTutCharacter :: UpdateCharText ( ) { //Create a string that will display the health and bomb count values FString NewText = FString ( "Health: " ) + FString :: SanitizeFloat ( Health ) + FString ( " Bomb Count: " ) + FString :: FromInt ( BombCount ) ; //Set the created string to the text render comp CharText -> SetText ( FText :: FromString ( NewText ) ) ; }

At this point, we have achieved a simple logic that initializes the BombCount and Health variables and updates the CharText each time the Health or the BombCount variables change. The last thing we need is an input that (for this part only) decreases the Bomb Count and applies damage to a player.

To apply damage to a character, we’re going to use the built-in TakeDamage function. As we already mentioned, since the damage is important to our gameplay, only the server will be able to apply any damage to any character. To handle this, each time a player attemps to damage another player, we’re going to perform a check to determine if the damage dealer is the server. If not, then the server will handle that functionality.

Please note that we will follow the same logic for the bomb spawn too. This means that if a client wants to spawn a bomb, he will tell the server to do so.

Inside the header file of your character, add the following logic:

Damage functions private: /** * TakeDamage Server version. Call this instead of TakeDamage when you're a client * You don't have to generate an implementation. It will automatically call the ServerTakeDamage_Implementation function */ UFUNCTION(Server, Reliable, WithValidation) void ServerTakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser); /** Contains the actual implementation of the ServerTakeDamage function */ void ServerTakeDamage_Implementation(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser); /** Validates the client. If the result is false the client will be disconnected */ bool ServerTakeDamage_Validate(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser); //Bomb related functions /** Will try to spawn a bomb */ void AttempToSpawnBomb(); /** Returns true if we can throw a bomb */ bool HasBombs() { return BombCount > 0; } /** * Spawns a bomb. Call this function when you're authorized to. * In case you're not authorized, use the ServerSpawnBomb function. */ void SpawnBomb(); /** * SpawnBomb Server version. Call this instead of SpawnBomb when you're a client * You don't have to generate an implementation for this. It will automatically call the ServerSpawnBomb_Implementation function */ UFUNCTION(Server, Reliable, WithValidation) void ServerSpawnBomb(); /** Contains the actual implementation of the ServerSpawnBomb function */ void ServerSpawnBomb_Implementation(); /** Validates the client. If the result is false the client will be disconnected */ bool ServerSpawnBomb_Validate(); public: /** Applies damage to the character */ virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 private : /** * TakeDamage Server version. Call this instead of TakeDamage when you're a client * You don't have to generate an implementation. It will automatically call the ServerTakeDamage_Implementation function */ UFUNCTION ( Server , Reliable , WithValidation ) void ServerTakeDamage ( float Damage , struct FDamageEvent const & DamageEvent , AController * EventInstigator , AActor * DamageCauser ) ; /** Contains the actual implementation of the ServerTakeDamage function */ void ServerTakeDamage_Implementation ( float Damage , struct FDamageEvent const & DamageEvent , AController * EventInstigator , AActor * DamageCauser ) ; /** Validates the client. If the result is false the client will be disconnected */ bool ServerTakeDamage_Validate ( float Damage , struct FDamageEvent const & DamageEvent , AController * EventInstigator , AActor * DamageCauser ) ; //Bomb related functions /** Will try to spawn a bomb */ void AttempToSpawnBomb ( ) ; /** Returns true if we can throw a bomb */ bool HasBombs ( ) { return BombCount > 0 ; } /** * Spawns a bomb. Call this function when you're authorized to. * In case you're not authorized, use the ServerSpawnBomb function. */ void SpawnBomb ( ) ; /** * SpawnBomb Server version. Call this instead of SpawnBomb when you're a client * You don't have to generate an implementation for this. It will automatically call the ServerSpawnBomb_Implementation function */ UFUNCTION ( Server , Reliable , WithValidation ) void ServerSpawnBomb ( ) ; /** Contains the actual implementation of the ServerSpawnBomb function */ void ServerSpawnBomb_Implementation ( ) ; /** Validates the client. If the result is false the client will be disconnected */ bool ServerSpawnBomb_Validate ( ) ; public : /** Applies damage to the character */ virtual float TakeDamage ( float Damage , struct FDamageEvent const & DamageEvent , AController * EventInstigator , AActor * DamageCauser ) override ;

Then, provide the following logic for each function:

float ANetworkingTutCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) { Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser); //Decrease the character's hp Health -= Damage; if (Health <= 0) InitHealth(); //Call the update text on the local client //OnRep_Health will be called in every other client so the character's text //will contain a text with the right values UpdateCharText(); return Health; } void ANetworkingTutCharacter::ServerTakeDamage_Implementation(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) { TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser); } bool ANetworkingTutCharacter::ServerTakeDamage_Validate(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) { //Assume that everything is ok without any further checks and return true return true; } void ANetworkingTutCharacter::AttempToSpawnBomb() { if (HasBombs()) { //If we don't have authority, meaning that we're not the server //tell the server to spawn the bomb. //If we're the server, just spawn the bomb - we trust ourselves. if (Role < ROLE_Authority) { ServerSpawnBomb(); } else SpawnBomb(); //todo: this code will be removed in the next part FDamageEvent DmgEvent; if (Role < ROLE_Authority) { ServerTakeDamage(25.f, DmgEvent, GetController(), this); } else TakeDamage(25.f, DmgEvent, GetController(), this); } } void ANetworkingTutCharacter::SpawnBomb() { //Decrease the bomb count and update the text in the local client //OnRep_BombCount will be called in every other client BombCount--; UpdateCharText(); //todo: spawn the actual bomb in the next part of the tutorial } void ANetworkingTutCharacter::ServerSpawnBomb_Implementation() { SpawnBomb(); } bool ANetworkingTutCharacter::ServerSpawnBomb_Validate() { return true; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 float ANetworkingTutCharacter :: TakeDamage ( float Damage , struct FDamageEvent const & DamageEvent , AController * EventInstigator , AActor * DamageCauser ) { Super :: TakeDamage ( Damage , DamageEvent , EventInstigator , DamageCauser ) ; //Decrease the character's hp Health -= Damage ; if ( Health <= 0 ) InitHealth ( ) ; //Call the update text on the local client //OnRep_Health will be called in every other client so the character's text //will contain a text with the right values UpdateCharText ( ) ; return Health ; } void ANetworkingTutCharacter :: ServerTakeDamage_Implementation ( float Damage , struct FDamageEvent const & DamageEvent , AController * EventInstigator , AActor * DamageCauser ) { TakeDamage ( Damage , DamageEvent , EventInstigator , DamageCauser ) ; } bool ANetworkingTutCharacter :: ServerTakeDamage_Validate ( float Damage , struct FDamageEvent const & DamageEvent , AController * EventInstigator , AActor * DamageCauser ) { //Assume that everything is ok without any further checks and return true return true ; } void ANetworkingTutCharacter :: AttempToSpawnBomb ( ) { if ( HasBombs ( ) ) { //If we don't have authority, meaning that we're not the server //tell the server to spawn the bomb. //If we're the server, just spawn the bomb - we trust ourselves. if ( Role < ROLE_Authority ) { ServerSpawnBomb ( ) ; } else SpawnBomb ( ) ; //todo: this code will be removed in the next part FDamageEvent DmgEvent ; if ( Role < ROLE_Authority ) { ServerTakeDamage ( 25.f , DmgEvent , GetController ( ) , this ) ; } else TakeDamage ( 25.f , DmgEvent , GetController ( ) , this ) ; } } void ANetworkingTutCharacter :: SpawnBomb ( ) { //Decrease the bomb count and update the text in the local client //OnRep_BombCount will be called in every other client BombCount -- ; UpdateCharText ( ) ; //todo: spawn the actual bomb in the next part of the tutorial } void ANetworkingTutCharacter :: ServerSpawnBomb_Implementation ( ) { SpawnBomb ( ) ; } bool ANetworkingTutCharacter :: ServerSpawnBomb_Validate ( ) { return true ; }

As a last step, provide the following input for your character:

PlayerInputComponent -> BindAction ( "ThrowBomb" , IE_Pressed , this , &ANetworkingTutCharacter :: AttempToSpawnBomb ) ;

At this point, if you test your project with two windows (one being the server and one being the client) you will have the same result as the video in the beginning of this post.