UE4 Networking

As stated previously, UE4 boasts a networking layer that closely matches a client/server pattern with its own solutions and adaptions to make it a great and robust networking layer, which we will use to create our first-person shooter! Thankfully, because of this layer, we do not have to implement our own connection/interpolation code, we can leave that all up to the engine. All we have to do is write our gameplay code with networking in mind. It is also important to note that this game will be designed to take place over LAN. There are some key UE4 networking concepts we must cover first.

Dedicated or listen servers

When working with UE4, we can choose to run a dedicated or listen server. The main difference between the two is that on a dedicated server, the server does not own its own character. On a listen server, the server will have its own character that it can control. This means we are able to have a server with its own controlled character interacting with the game world and other players. Sometimes this is commonly referred to as the host in any multiplayer match, and these players will have a distinct advantage over connected players (no latency).

A dedicated server, on the other hand, has no such thing and is usually a non-visual version of the game world and is purely used for maintaining and updating the game world. When deciding which of the two to use, you must do so early on as they require very different coding methodologies. For the purposes of this chapter, we will be designing a listen server for ease of use.

UE4 replication

As stated previously in Networking Theory section, there will be networked objects whose information will be shared among all connected clients and the server. These objects are known as replicated objects. Within the UE4 hierarchy, anything that inherits from AActor can be replicated. This can be done by setting the AActor member bReplicates to true. This will flag the actor for replication and will be considered when the game performs a net update. A net update is the stage in which the engine will update all networking-related entities and objects.

Replicated objects can also specify which of their internal members are to be replicated via the UPROPERTY() macro and the Replicated property specifier. For example, if you wished to have a float value replicated that represents a character's health, you would declare the variable as such:

UPROPERTY(Replicated)
float CharacterHealth;

This would then register the variable for replication and, as long as the owning object is also replicated, it will be updated to all connected clients through the network. Any member that does not have this flag specified will not replicate. It is important to remember this. When the Unreal Build Tool/Unreal Header Tool encounters a replicated specifier in a header file, it will automatically add the following function declaration to the class definition at code generation time:

virtual void AYourActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const

We must then define this function in the .cpp of any replicated objects that contain replicated members. For the previous example, the definition would appear as follows:

{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(AYourActor, CurrentHealth);
}

Within this definition, we have included a call to the super classes implementation of the function, then used the macro DOREPLIFETIME. This macro takes in the class type that the variable is owned by and the variable itself. This will replicate the CurrentHealth variable whenever it is changed over its lifetime. DOREPLIFETIME is optimized to only send a network update when the variable is changed. There are also multiple conditions that we can set when replicating variables. This is done using the DOREPLIFETIME_CONDITION macro. We will not be covering this facet of the UE4 networking layer but more information can be found at https://docs.unrealengine.com/latest/INT/Gameplay/Networking/Actors/Properties/Conditions/index.html.

Network role

Now that we have replicated actors, we need to cover network roles. For every replicated actor, there are three roles that can be assumed—Role_Authority, Role_AutonomousProxy, and Role_SimulatedProxy. Which role a replicated actor assumes is very important:

  • Role_Authority: It will always be the role of the version of a replicated actor that exists on the server. This is very important as it means that only the server has authority over the variables and states of this actor.
  • Role_AutonomousProxy: It is assumed by the version of an actor that exists on the owning client. This means that the client can do things such as move the character around, send and receive direct RPCs to/from the server, and the player's input will have an effect on the server's interpolation of the actor position.
  • Role_SimulatedProxy: It is assumed by the version of an actor that exists on all other clients; this is purely a simulation and will have no power over the state of the actor.

For example, referencing the client/server diagram used earlier, computer A requests the server to spawn a first-person character that it will control called Character A. The role of Character A on the owning client is Role_AutonomousProxy. The owning client being the one that created/requested the controlled character in the first place. The role of Character A that is created on the server is Role_Authority, and the role of Character A that is created on all other clients that can't control the character (computers B through D) is Role_SimulatedProxy.

The thing that these roles mostly represent is network ownership. For any replicated actor, there will always be a server owner actor (Role_Authority). There may or may not be a client owner of that actor (Role_AutonomousProxy) and there may or may not be a non-owning client with an instance of that actor (Role_SimulatedProxy).

Remote Procedure Calls (RPCs)

RPCs are the way in which we instruct the server or client versions of an object to execute functionality across a network. They are called locally but are executed remotely on another machine's object instance. For example, if we were to receive an OnFire() input command on a client, the RPCs would appear as follows:

  • OnFire() executed locally on client: Begin local animation and effects, inform server of a fire
  • RPC rom client to server: FireMyWeapon()
  • Fire My Weapon received on server: Perform ray-trace to see whether anything was hit and inform clients to play PlayerFired visuals
  • RPC From Server To All connected Clients: PlayerFiredVisuals()
  • PlayerFiredVisuals Received on all connected Clients: Spawn and play particle emitters for gunshot and sounds for gunshot

Previously, we have an owning-client instance of a character receiving an OnFire() event (the player pressed the shoot button). The client will play local animation and effects that will not be seen by other players (first-person effects), then informs the server instance of the character to fire its weapon via an RPC. Note that as this is the owing client of the character, it is able to send this RPC to the server instance of this object.

This RPC will instruct the server instance of the character to perform the ray-trace calculations. As all of the characters found on the server are authority instances, we can assume that the positional information of these characters is correct. Also, upon receiving this FireMyWeapon(), RPC calls the server instance and informs all connected clients to play the third-person visual effects. This is important, as the other connected clients must witness the Role_Simulated Proxy version of the character fire its weapon. As we wish these effects to play on all client instances of a character, we must not use a direct client RPC (as that would only execute on a Role_AutonomousProxy client).

Types of RPC

Within the Unreal Networking layer, RPCs are declared via the UFUNCTION macro. There are three types of RPC and they are declared as follows:

UFUNCTION( Client )
void ClientRPCFunction();

UFUNCTION( Server )
void ServerRPCFunction();

UFUNCTION( NetMulticast )
void MulticastRPCFunction();

First, I will describe the traditional uses of these three RPCs then describe what will happen if they are called from an actor with the incorrect network role/ownership:

  • Client: A client RPC should be called from the server and will be executed on the owning client (client instance with Role_AutonomousProxy). If a client RPC is invoked on a client or non-owning client, the function will only be called locally.
  • Server: A server RPC should be called from the owning client. If so, the RPC will execute on the server. If it is invoked from a non-owning client (client instance with Role_SimulatedProxy), the function will be dropped and not executed.
  • NetMultiCast: This is an interesting RPC as, if it is invoked from a server, it will be executed on both the server and all connected clients, even those with Role_SimulatedProxy. This is useful for functions you wish to execute on every instance of a replicated actor. If a NetMultiCast RPC is called from a client, non-owning or owning, it will only execute locally.

RPCs are unreliable by default and we must also specify Reliable in the UFUNCTION macro. If we want a validation check on an RPC before it is executed, we can include WithValidation to the UFUNCTION macro as well.

Defining RPCs

When the UBT/UHT encounters a function with the any of the RPC specifiers in the UFUNCTION macro, it will automatically replace that function declaration with generated versions depending on the contents of the UFUNCTION macro. Let's take ClientRPCFunction as an example. It would be replaced with the following:

void ClientRPCFunction_Implementation();

If we were to specify WithValidation as well, the following would also be added:

bool ClientRPCFunction_Validate();

Therefore, when we define our RPCs, we must do so with the previous function declarations in mind, otherwise we will receive linker errors when we try to build the project. With the previous example, the definitions in the .cpp would appear as follows:

void ClientRPCFunciton_Implementation()
{
    // Do something on the client
}

Bool ClientRPCFunction_Validate(){bool bSomeCheck = SomeCheck();
    return bSomeCheck;
}

Net ownership, player controllers and game modes

We have briefly described network ownership through the use of network roles, but now it is time to be more specific. The main difference between an owning client and a non-owning client is the existence of a player controller. Player controllers are only created on the owning-client and that player controller is the channel through which all communication with the server takes place.

For example, computer A has the PlayerController for character A. All other versions of Character A on computers B through D do not have a PlayerController for Character A. Intern computer A does not have a PlayerController for any of the other computers either. This is a very important concept to understand as a connection to a server is done through the PlayerController. Each controller is replicated between the server and owning client.

In the same vein, the GameMode is only created on the network authority. In our case, that is the server. That is why when we have tried to get the GameMode from the game world, thus far we have called GetAuthGameMode(), or in long form, GetAuthorityGameMode. In a networked project, that function will return null on all clients.

The way that we then communicate game and player information between all clients and servers is via GameState and PlayerState objects. These two objects are replicated to all connected clients. It is also important to separate PlayerState from character state. A player state is information pertaining to the player that is connected to the server, not the character it is controlling. The same thing is said for the GameState; if we wish to parse game information to the connected clients, we must do so through a custom GameState object.

To summarize the idea of ownership in more clear terms, for every client that is connected to a server session, there will now be a series of new instances of the character object. Computer A will have the owning-client version of its own character, Character A. Computers B through D will now also have an instance of Character A; however, these will be non-owning versions (Role_SimulatedProxy). There is a final version of Character A; this one exists on the server and is the authority instance of the character. Meaning that if any changes are made to the authority, it will be replicated to all other connected client versions of Character A, including the owning-client instance. And, as stated previously, this will be the same for all of the other player characters added by computers B through D.

For each connected computer or player, there will also be a unique player state that is replicated to all connected network entities (servers and clients). For each multiplayer game session, there will be one game state that is also replicated to all network entities.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.107.142