Game modes and servers

Alright, finally we can create our game mode. For this network shooter, our game mode is going to control traveling between levels, assigning teams, and spawning/respawning players. To do this, our game mode is going to have to detect a few things. Firstly, we are going to have to override how the game mode detects a new player joining the server session. We also have to make sure we assign this new player a team and spawn them properly. We already have a GameMode generated for us that was created with the template.

Game mode class definition

Navigate to NSGameMode.h and modify the class definition underneath our ETeam enum so it matches the following:

UCLASS(minimalapi)
class ANSGameMode : public AGameMode
{
    GENERATED_BODY()

public:
    ANSGameMode();
    virtual void BeginPlay() override;
    virtual void Tick(float DeltaSeconds) override;
    virtual void PostLogin(APlayerController* NewPlayer) override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) 
override;

    void Respawn(class ANSCharacter* Character);
    void Spawn(class ANSCharacter* Character);

private:
    TArray<class ANSCharacter*> RedTeam;
    TArray<class ANSCharacter*> BlueTeam;

    TArray<class ANSSpawnPoint*> RedSpawns;
    TArray<class ANSSpawnPoint*> BlueSpawns;
    TArray<class ANSCharacter*> ToBeSpawned;

    bool bGameStarted;
    static bool bInGameMenu;
};

Here we have overridden a few of the functions found in the AGameMode interface. BeginPlay() is going to let us initialize some of the default behaviors for the game mode, such as find all available spawn points. Tick() will let us check for input and spawn any queued characters. PostLogin() is the method that will be called whenever a client joins the server session; this method takes in handle to the APlayerController of the client that just joined. It is with this function that we will execute functionality on the player controller so we can prepare that player for play. EndPlay() will be called when a game session is ended; we will be able to interpret how to treat ANSGameMode static members when this happens.

Following our virtual functions, we have Spawn() and Respawn().These methods will all be used to spawn players; the respawn method is the one referenced in the ANSCharacter that must be uncommented. The members of this object include five arrays that we will use to store characters from both teams, spawn points from both teams, and any characters that are queued to be spawned by the game mode. Lastly, we have some Boolean flags that will determine the state of the game, one of them being declared as static as we wish this Boolean to remain in memory during server travel, which we will cover later.

Construction the Game mode and Finding Spawn points

Navigate to NSGameMode.cpp now and ensure the include list at the top of the file matches the following:

#include "NS.h"
#include "NSGameMode.h"
#include "NSHUD.h"
#include "NSPlayerState.h"
#include "NSSpawnPoint.h"
#include "NSCharacter.h"

Let's start defining our game mode functions with the constructor. Modify the generated code present in NSGameMode.cpp to the following:

bool ANSGameMode::bInGameMenu = true;

ANSGameMode::ANSGameMode()
    : Super()
{
    // set default pawn class to our Blueprinted character
static ConstructorHelpers::FClassFinder<APawn> PlayerPawnClassFinder(TEXT
("/Game/FirstPersonCPP/Blueprints/FirstPersonCharacter"));

    DefaultPawnClass = PlayerPawnClassFinder.Class;
    PlayerStateClass = ANSPlayerState::StaticClass();

    // use our custom HUD class
    HUDClass = ANSHUD::StaticClass();

    bReplicates = true;
}

The most important point to note about this definition is the initialization of our static bool bInGameMenu. This bool has been declared as static so that its state will persist through server travels to new maps. As you can see, we have had to initialize this static memory as you normally would. Within the game mode itself, we simply set the default pawn class to whatever generated class is found at our FirstPersonCharacter Blueprint location. We then set the player state class and HUD class to the appropriate defaults then set the game mode to replicate. Ensure you add #include. Now let's work with BeginPlay():

void ANSGameMode::BeginPlay()
{
    Super::BeginPlay();

    if (Role == ROLE_Authority)
    {
        for (TActorIterator<ANSSpawnPoint> Iter(GetWorld()); Iter; 
        ++Iter)
        {
            if ((*Iter)->Team == ETeam::RED_TEAM)
            {
                RedSpawns.Add(*Iter);
            }
            else
            {
                BlueSpawns.Add(*Iter);
            }
        }

        // Spawn the server
        APlayerController* thisCont = GetWorld()->
        GetFirstPlayerController();

        if (thisCont)
        {
            ANSCharacter* thisChar = Cast<ANSCharacter>(thisCont->
            GetPawn());

            thisChar->SetTeam(ETeam::BLUE_TEAM);
            BlueTeam.Add(thisChar);
            Spawn(thisChar); 
        }

    }
}

Even though the game mode will only exist on the server, it is still important to check authority. We are iterating over all ANSSpawnPoint objects in the scene via an ActorIterator template class. We then check each spawn point to see which team it is associated with, then sort it into the appropriate array. We do this so we can perform generic spawn logics on both containers. Finally, we spawn the server character; we have to do this manually as the server will not connect to itself, thus a post-login call will not be made. It is also important to note that there will be no Role_Autonomous proxy instance for this character as there is no owning-client, which would be the server itself.

Ending the Game and ticking the game mode

Next, we must provide a definition for the virtual EndPlay() method:

void ANSGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (EndPlayReason == EEndPlayReason::Quit || 
    EndPlayReason == EEndPlayReason::EndPlayInEditor)
    {
        bInGameMenu = true;
    }
}

Here we simply ensure that play is not being ended for map traveling reasons and that it is in fact either a definite quit event or end PIE session event. If so, we are to reset our static Boolean so the game always starts in menu. Following this, we will be defining our Tick() method. This method will handle checking whether there are any players that need to be spawned and whether the R key has been pressed. If so, we will travel the server sessions to the map we have been working from. Add the following code to the .cpp:

void ANSGameMode::Tick(float DeltaSeconds)
{

    if (Role == ROLE_Authority)
    {
        APlayerController* thisCont = GetWorld()->
        GetFirstPlayerController();



        if (ToBeSpawned.Num() != 0)
        {
            for (auto charToSpawn : ToBeSpawned)
            {
                Spawn(charToSpawn);
            }
        }

        if (thisCont != nullptr && thisCont->IsInputKeyDown(EKeys::R))
        {
            bInGameMenu = false;
            GetWorld()->ServerTravel(
            L"/Game/FirstPersonCPP/Maps FirstPersonExampleMap?Listen");
        }
    }
}

Here we can see that we check again to ensure this is executing on only the authority, even though the game mode itself should only ever exist on the server. The first thing we do is retrieve the player controller from the game world. We then check whether our ToBeSpawned container is not empty. If so, we iterate over all characters to be spawned and invoke the Spawn() function. This will ensure that any characters that need to spawn that round will do so. We are doing this check in the tick to ensure that any spawns rejected due to blocked spawn points will still be spawned eventually.

After this, we then check that the player controller we obtained is valid and that the server player has pressed the R key. If so, we wish to travel the server. We are doing this by calling the method ServerTravel(). This method will travel the server to the provided map URL; it will also travel all connected clients to the map. You may have noticed that we have appended the map URL with ?Listen. This is an extra command that ensures that the map the server is traveling to will accept connecting clients.

The important thing to note is that, when the server travels the player states, player controllers and game states all persist through traveling.

For more on traveling within networked multiplayer games, visit https://docs.unrealengine.com/latest/INT/Gameplay/Networking/Travelling/.

Connecting players

We are now about to write how we handle players connecting to our game session. Whenever a new player joins the game, we want to assign that player a team and ensure that the new player is queued to spawn. This spawning will take place on the server as replicated actors should only be created/destroyed by the server. We are going to do this via the PostLogin() method we declared earlier. As stated previously, this method will be called whenever a new client joins the server session and will parse the new PlayerController that has connected. Add the following function definition to the .cpp:

void ANSGameMode::PostLogin(APlayerController* NewPlayer)
{
    Super::PostLogin(NewPlayer);

    ANSCharacter* Teamless = Cast<ANSCharacter>(NewPlayer->GetPawn());
    ANSPlayerState* NPlayerState = Cast<ANSPlayerState>(NewPlayer-> 
    PlayerState);

    if (Teamless != nullptr && NPlayerState != nullptr)
    {
        Teamless->SetNSPlayerState(NPlayerState);
    }
    // Assign Team and spawn
    if (Role == ROLE_Authority && Teamless != nullptr)
    {
        if (BlueTeam.Num() > RedTeam.Num())
        {
            RedTeam.Add(Teamless);
            NPlayerState->Team = ETeam::RED_TEAM;
        }
        else if (BlueTeam.Num() < RedTeam.Num())
        {
            BlueTeam.Add(Teamless);
            NPlayerState->Team = ETeam::BLUE_TEAM;
            }
        else // Teams are equal
        {
            BlueTeam.Add(Teamless);
            NPlayerState->Team = ETeam::BLUE_TEAM;
        }

        Teamless->CurrentTeam = NPlayerState->Team;
        Teamless->SetTeam(NPlayerState->Team);
        Spawn(Teamless);
    }
}

Here we are ensuring that the character type that is owned by the parsed controller is of type ANSCharacter; we also ensure that the player state of the controller is also of type ANSPlayerState. We do this via casting both and checking that the casts were valid. Following this, we simply assign the new player state to the connected character. We then check the size of the red and blue team arrays. If one is smaller than the other, they will be assigned the new player, and the player will have its team flag set in both the character and the player state. If the teams are the same size, the player will be given to the blue team. Lastly, we pass the newly connected character to the Spawn() function.

Spawning the players

Our spawn functionality will be fairly simple; we do not have to do anything special to spawn networked characters. We are simply going to check the team of the player to be spawned, ensure the spawns for that team are unblocked, and spawn a player. Add the following function to the .cpp:

void ANSGameMode::Spawn(class ANSCharacter* Character)
{
    if (Role == ROLE_Authority)
        
        // Find Spawn point that is not blocked
        ANSSpawnPoint* thisSpawn = nullptr;
        TArray<ANSSpawnPoint*>* targetTeam = nullptr;

        if (Character->CurrentTeam == ETeam::BLUE_TEAM)
        {
            targetTeam = &BlueSpawns;
        }
        else
        {
            targetTeam = &RedSpawns;
        }

        for (auto Spawn : (*targetTeam))
        {
            if (!Spawn->GetBlocked())
            {
                // Remove from spawn queue location
                if (ToBeSpawned.Find(Character) != INDEX_NONE)
                {
                    ToBeSpawned.Remove(Character);
                }

                // Otherwise set actor location
                Character->SetActorLocation(Spawn->
                GetActorLocation());

                Spawn->UpdateOverlaps();

                Return;
}
        }

        if (ToBeSpawned.Find(Character) == INDEX_NONE)
        {
            ToBeSpawned.Add(Character);
        }
    }
}

Here we again check authority. We then declare two handles, one for an ANSSpawnPoint and another for a TArray that contains ANSSpawnPoint handles. We then assign the address of the appropriate spawn array based off of the player's team type. We then iterate over that array and check whether the spawn is not blocked. If we find a non-blocked spawn, we check whether the player is contained within the ToBeSpawned array. If so, we remove from the array before spawning, as this means it was blocked from spawning at the last attempt.

The act of spawning is very simple, as the character will have already been created in memory when the client joined; we simply set the actor location to be that of the spawn point. We then update the overlaps on the spawn to ensure it detects the new actor. We then return out of the function to cease any further execution.

If the function continues, this means the player was not spawned and should be added to the ToBeSpawned array. We check that the player does not already exist in the array and add it.

Respawning the player

Respawning will be a little different, as we need to be able to reset not only the player states, but also the states of the components on ALL clients. When dealing with things such as ragdoll, this can lead to some serious desynching of assets. Instead we will be creating brand new objects and destroying the old ones. Note that this does incur a cost on respawn and may be undesirable for more complicated memory-intensive actors, but works just fine for our needs. We will do this with the Respawn() method that is called internally by the ANSCharacter. Add the following function definition to the .cpp:

void ANSGameMode::Respawn(class ANSCharacter* Character)
{
    if (Role == ROLE_Authority)
    {
        AController* thisPC = Character->GetController();
        Character->DetachFromControllerPendingDestroy();

        ANSCharacter* newChar = Cast<ANSCharacter>(GetWorld()->
       SpawnActor(DefaultPawnClass));

        if (newChar)
        {
            thisPC->Possess(newChar);
            ANSPlayerState* thisPS = Cast<ANSPlayerState>(newChar->
            GetController()->PlayerState);

            newChar->CurrentTeam = thisPS->Team;
            newChar->SetNSPlayerState(thisPS);

            Spawn(newChar);

            newChar->SetTeam(newChar->GetNSPlayerState()->Team);
        }
    }
}

The important thing to remember when addressing this function is that everything between an owning-client and server is invoked through the player controller. This means, as long as we have a handle to the player controller of the connected client, we can inform changes to the player. For example, we can now spawn a new character on the server and assign it to the player controller. This will shift the ownership for the character spawned on the server to be that of the player controller (and the owning client). The server will still be the authority for this new player.

In the previous function, we ensure that the current role is authority. If so, we are to respawn the provided character. We then get the controller from the character to be spawned and then call DetachFromControllerPendingDestroy(). This will detach the old character pawn from the player controller, this will flag that old pawn for destroy and free up the controller for a new character. This will also occur on the client, so be warned, if for some reason the new character is not spawned and assigned to the controller, the client will be left characterless!

Following this, we then spawn a new ANSCharacter via the game world and parse the default pawn class to the SpawnActor() method (this will ensure that our blueprint abstraction is spawned and not the base class). If this character was spawned successfully, we then ensure that the player controller possesses this new ANSCharacter and that we assign the ANSPlayerState to a new character. This will guarantee that the values saved in the player state are accessible by the new character. Following this, we call Spawn() on this new character so that it is positioned properly, then call SetTeam() on the new character so that the mesh materials are colored properly.

Now, quickly navigate back to the NSCharacter.cpp and go to the Respawn() method and uncomment the previously commented respawn line. It will appear as follows:

void ANSCharacter::Respawn()
{
    if (Role == ROLE_Authority)
    {
        // Get Location from game mode
        NSPlayerState->Health = 100.0f;
        Cast<ANSGameMode>(GetWorld()->GetAuthGameMode())->
        Respawn(this);
    
        Destroy(true, true);
    }
}
..................Content has been hidden....................

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