While the modern internet allows gamers to connect to each other all over the world, the internet is sometimes not as stable as we might wish. Disconnections during internet-based gameing happens, and if you don't plan for it in your games, your players suffer. Thankfully, UE4 has your back, and makes it pretty easy enough to support resuming roughly where they left off after reconnecting. It's not perfect, but will likely be good enough for most games. Let's dive into how to set this up for your game. In this section assume you have a basic grasp of C++ in the following section, but it's likely possible to implement most of these systems in Blueprint by implementing overrides of the functions listed, in your PlayerState's EventGraph.

When a player disconnects from the game server, their PlayerState object is removed from the PlayerArray on the GameState, and then a copy is created and added to an array on the GameMode1 intuitively called InactivePlayerArray. After this copy is made, the original PlayerState is destroyed.

When that same player reconnects, UE4 will attempt to match2 them with a PlayerState stored in the InactivePlayerArray. If there's a match, UE4 will use the stored PlayerState instead of using a new one of that player.

Do you remember when I mentioned that UE4 makes a copy of the PlayerState when a player disconnects? Well, that's only partially true. By default, UE4 will only copy some variables such as PlayerName, PlayerId, Score, UniqueNetId, and a few others. Unfortunately, this process has no idea about your game's custom variables. Thankfully we have a way of hooking into this process and copying the variables we want to persist ourselves.

Let's imagine we've defined our own PlayerState for our game with some custom member variables:

// MyPlayerState.h
UCLASS()  
class AMyPlayerState: public APlayerState  
{
    GENERATED_BODY()

public:  
    /** The amount of kills this player has */
    UPROPERTY(Transient, Replicated, BlueprintReadWrite)
    int32 Kills;

    /** The amount of deaths this player has */
    UPROPERTY(Transient, Replicated, BlueprintReadWrite)
    int32 Deaths;

    /** The amount of money this player has */
    UPROPERTY(Transient, Replicated, BlueprintReadWrite)
    int32 Money;

    /** The hero the player has selected to (re)spawn as */
    UPROPERTY(Transient, Replicated, BlueprintReadWrite)
    TSubclassOf<APawn> SelectedHero;

public:  
    // Class methods Here...

};

By default, none of these properties will persist after a player disconnects. To resolve this problem, we need to define a function named CopyProperties(APlayerState*) in our header file MyPlayerState.h.

virtual void CopyProperties(APlayerState* NewPlayerState) override;  

Once we have defined that in our header file, we must add our implementation in our MyPlayerState.cpp file. That might look something like the following:

// MyPlayerState.cpp
void AMyPlayerState::CopyProperties(APlayerState* NewPlayerState)  
{
    Super::CopyProperties(NewPlayerState);

    AMyPlayerState* MyNewPlayerState = Cast<AMyPlayerState>(NewPlayerState);
    if (MyNewPlayerState)
    {
        MyNewPlayerState->Kills = Kills;
        MyNewPlayerState->Deaths = Deaths;
        MyNewPlayerState->Money = Money;
    }
}

In this example, we copied our first three members, Kills, Deaths, and Money, but did not copy the SelectedHero property, so the player will need to set that following normal game-flow.

There's only one more step we need to make sure we follow, or we will run into issues later on when our server decided to change maps. During a Seamless Server Travel, PlayerState values are copied and persist to the next map. To do this, it uses the same function we override earlier, CopyProperties(APlayerState*). Depending on your game, you might want some of these values to persist, but you will likely want at least some of them to be reset, and Reset we shall!

There is another method that we will define, called during the SeamlessTravel process, named Reset(). In our header file MyPlayerState.h, it'll look like the following:

virtual void Reset() override;  

The implementation in our MyPlayerState.cpp file will look something like the following:

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

    Kills = 0;
    Deaths = 0;
    Money = 800;
    SelectedHero = nullptr;
}

This will reset all of our values to their defaults and will allow us to start fresh on our new map.

It is worth noting that the InactivePlayerArray will only store each PlayerState for a specific amount of time. The default in the base GameMode is 300 seconds, but you can set this value to any value you like, by setting the value of InactivePlayerStateLifeSpan in your GameMode. The value is the amount of seconds after being set as inactive that the PlayerState will persist. You can also set this value to '0' to disable this behaviour. If the player has not reconnected by then, their stored state will automatically be destroyed.

Well, that about wraps up this. If you have any questions or comments, feel free to leave a comment below. Thanks for reading!

Notes

  1. The PlayerState is only copied if the player was not flagged through the 'bOnlySpectator' state, which implies they may only watch the game as a specatator, and cannot participate.

  2. Unfortunately the current method of matching a player with a previous PlayerState is through a comparison of their IP Address. If multiple players are playing from the same remote IP Address, it is possible that the wrong player can takeover for another disconnected player on their network. Oddly enough, this does not affect Console builds of UE4 Games, as they instead check the 'UniqueNetId' of players, which is a better unique identifier.