Building GameModeBase #

Now that we have the foundational pieces in place, we can turn our attention to the higher-level functionality necessary for our game server to work successfully with the Pragma Engine backend. In this section, we’ll be working primarily in TutorialsGameModeBase.h and TutorialsGameModeBase.cpp, so open those files in your IDE now.

Import headers #

In your header file, ensure that your includes look like the following:

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PragmaGameServer.h"
#include "TutorialsGameModeBase.generated.h"

Build out GameModeBase #

  1. Execution of GameModeBase starts from the BeginPlay() function. Ensure that your BeginPlay() contains the following code:
// Called when the game starts or when spawned
void ATutorialsGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	if (!GetWorld()->IsNetMode(NM_DedicatedServer))
	{
		return;
	}
	
	GameServer = NewObject<UPragmaGameServer>(this);
	GameServer->OnGameStartDataReceived.AddUObject(this, &ATutorialsGameModeBase::HandleGameStartDataReceived);
	GameServer->Start();

	auto* GameMode = Cast<ATutorialsGameModeBase>(GetWorld()->GetAuthGameMode());
	GameMode->OnPlayerConnected.AddUObject(this, &ATutorialsGameModeBase::HandlePlayerConnected);
	GameMode->OnGameEnded.AddUObject(this, &ATutorialsGameModeBase::EndGame);
}
  1. We instantiate UPragmaGameServer, which we built in the last section of this guide, and call its Start() function. We also register several event handlers, so let’s go ahead and declare those now.

In the header file, declare HandleGameStartDataReceived(), HandlePlayerConnected(), and EndGame():

public:
	void EndGame();

protected:
	void HandleGameStartDataReceived(FPragma_GameInstance_GameStart GameStartData);
	void HandlePlayerConnected(APlayerController* NewPlayer) const;
  1. Switch back to TutorialsGameModeBase.cpp and define HandleGameStartDataReceived():
void ATutorialsGameModeBase::HandleGameStartDataReceived(FPragma_GameInstance_GameStart GameStartData)
{
	StoredGameStartData = GameStartData;
	
	// Here you can continue game server startup with game-specific information, then call ConnectPlayers once ready to accept players.
	// GameStartData has additional information about the players expected to be connecting, so you can prepare loadouts, reject players, etc.

	FPragma_GameInstance_ConnectPlayersV1Request Request;
	Request.GameInstanceId = GameStartData.GameInstanceId;
	Request.Hostname = "127.0.0.1";
	Request.Port = 7777;
	
	for (const auto& Player : GameStartData.Players)
	{
		auto i = Request.PlayerConnectionDetails.Emplace();
		Request.PlayerConnectionDetails[i].PlayerId = Player.PlayerId;
		// Request.PlayerConnectionDetails[i].ExtPlayerConnectionDetails = TEXT("your player-specific data here");
	}

	GameServer->GetServer()->MatchApi().ConnectPlayers(
		Request.GameInstanceId,
		Request.PlayerConnectionDetails,
		Request.Hostname,
		Request.Port,
		UPragmaGameInstancePartnerServiceRaw::FConnectPlayersV1Delegate::CreateUObject(
			this, &ATutorialsGameModeBase::HandleConnectPlayersResponse)
	);
}
HandleGameStartDataReceived overview
This is where the game server prepares connection information and any player-specific data. Afterward, we call ConnectPlayers to let the Pragma Engine backend know that the game server is ready to accept players. The backend then provides connection information to the game clients, which then connect to the game server.
  1. We have a reference to HandleConnectPlayersResponse, so let’s declare that in TutorialsGameModeBase.h:
protected:
    void HandleConnectPlayersResponse(TPragmaResult<FPragma_GameInstance_ConnectPlayersV1Response> Result, const FPragmaMessageMetadata& Metadata);
  1. Then back in TutorialsGameModeBase.cpp:
void ATutorialsGameModeBase::HandleConnectPlayersResponse(TPragmaResult<FPragma_GameInstance_ConnectPlayersV1Response> Result,
	const FPragmaMessageMetadata& Metadata)
{
	if (Result.IsFailure())
	{
		SERVER_ERR("ConnectPlayers failed!");
		GameServer->ShutdownServer();
		return;
	}
	
	// At this point we're ready to receive players.
	// Set a timeout in case anything else goes wrong, we automatically shutdown after some time so we don't sit idle on the machine.
	FTimerHandle TimeoutHandle;
	GetWorld()->GetTimerManager().SetTimer(TimeoutHandle, FTimerDelegate::CreateWeakLambda(this, [this]()
	{
		SERVER_LOG("Shutting down after timeout.");
		GameServer->ShutdownServer();
	}), 60.f * 5.f /* 5 min */, false);
}

If ConnectPlayers does not succeed, we shut down the server. Otherwise, we shut down the game server after 60 seconds of idle time to prevent an unused game server from occupying capacity indefinitely.

  1. Earlier, we declared HandlePlayerConnected() and EndGame(), so let’s provide the definitions for these two functions.
void ATutorialsGameModeBase::HandlePlayerConnected(APlayerController* NewPlayer) const
{
	SERVER_LOG("Player connected!");
	Cast<ATutorialPlayerController>(NewPlayer)->ClientNotifyConnected();
}

void ATutorialsGameModeBase::EndGame()
{
	SERVER_LOG("EndGame() called.");
	if (StoredGameStartData.IsSet())
	{
		TArray<FPragma_GameInstance_PlayerGameResult> PlayerGameResults;
		for (const auto& Player : StoredGameStartData->Players)
		{
			auto i = PlayerGameResults.Emplace();
			PlayerGameResults[i].PlayerId = Player.PlayerId;
		}

		GameServer->GetServer()->MatchApi().EndGame(
			StoredGameStartData->GameInstanceId,
			TArray<FPragma_GameInstance_PlayerGameResult>{},
			FPragma_GameInstance_ExtEndGameRequest{},
			UPragmaGameInstancePartnerServiceRaw::FEndGameV1Delegate::CreateUObject(this, &ATutorialsGameModeBase::HandleEndGameResponse)
		);
	}
	else
	{
		SERVER_ERR("No match data when trying to end game.")
		GameServer->ShutdownServer();
	}
}
  1. EndGame() has a reference to HandleEndGameResponse, so let’s declare and define that. In the header file:
protected:
	void HandleEndGameResponse(TPragmaResult<FPragma_GameInstance_EndGameV1Response> Result, const FPragmaMessageMetadata& Metadata) const;
  1. And in TutorialsGameModeBase.cpp:
void ATutorialsGameModeBase::HandleEndGameResponse(TPragmaResult<FPragma_GameInstance_EndGameV1Response> Result,
	const FPragmaMessageMetadata& Metadata) const
{
	if (Result.IsSuccessful())
	{
		SERVER_LOG("EndGame sent successfully");
	}
	else
	{
		SERVER_ERR("EndGame failed");
	}
	GameServer->ShutdownServer();
}
End game overview
EndGame() is where we collate match data when a game endds and send it off to the Pragma Engine backend via calling EndGameV1. EndGameV1 then calls HandleEndGameResponse(), where we can do additional tasks depending on whether EndGameV1 succeeded or failed, although in this example we’re simply printing to the log and then shut down the game server if the request failed.

Update the game client #

This concludes the work on the game server side of things. Now we have to make a small modification to the game client so that it knows what to do when it receives the host connection details notification.

  1. Open TutorialPlayerController.h and TutorialPlayerController.cpp.

  2. In the header file, declare HandleConnectionDetailsReady():

private:
    void HandleConnectionDetailsReady(const FPragma_GameInstance_HostConnectionDetailsV1Notification HostConnectionDetailsV1Notification);
  1. Then in TutorialPlayerController.cpp, define HandleConnectionDetailsReady():
void ATutorialPlayerController::HandleConnectionDetailsReady(const FPragma_GameInstance_HostConnectionDetailsV1Notification HostConnectionDetailsV1Notification)
{
	UE_LOG(LogTemp, Display, TEXT("Received Connection Details notification, attempting to connect to %s"), *HostConnectionDetailsV1Notification.ConnectionDetails.Hostname);
	ConnectToGameServer(HostConnectionDetailsV1Notification.ConnectionDetails.Hostname);
}
  1. In the player controller’s Init() function, append the following line to the end to register HandleConnectionDetailsReady() to the OnHostConnectionDetails event:
Player->GameLoopApi().OnHostConnectionDetails.AddUObject(this, &ATutorialPlayerController::HandleConnectionDetailsReady);
  1. HandleConnectionDetailsReady() calls ConnectToGameServer, so provide the following declaration in the header:
public:
    UFUNCTION(BlueprintImplementableEvent, Category = "Widget")
	void ConnectToGameServer(const FString& ServerAddress);

As you can see, this is a BlueprintImplementableEvent, so we’ll build the function in Unreal Editor shortly. Note that if Unreal Editor is currently open, you may need to first close it, recompile your game project, then relaunch Unreal Editor.

  1. In the header file, declare ClientNotifyConnected():
public:
	UFUNCTION(Client, Reliable)
	void ClientNotifyConnected();
  1. Then in TutorialPlayerController.cpp, define ClientNotifyConnected():
void ATutorialPlayerController::ClientNotifyConnected_Implementation()
{
	if (IsLocalPlayerController())
	{
		UE_LOG(LogTemp, Display, TEXT("Connected to game server!") );
	}
}

Implement the ConnectToGameServer Blueprint function #

  1. Open the player controller blueprint and add the Connect to Game Server event to the event graph.
  2. Create a new Blueprint function in the player controller Blueprint. Name it Connect to server.
  3. Double click the function to edit it.
  4. Create an input named Address of type String.
  5. Right-click an empty area of the blueprint and add a node for Open Level (by Name).
  6. Connect the nodes together.
    Unreal Blueprint for Connect to Server UFunction
  7. Switch back to the player controller event graph right click an empty area. Add a Connect to server node.
  8. Connect the Connect to game server event to the Connect to server function.
    Player controller Blueprint for Connect to Server UFunction
  9. Save your changes to the player controller Blueprint.

Now, when the client receives the host connection details notification, it will attempt to connect to the game server based on the connection information it receives.

Set the ServerMap GameMode Override #

Because the ServerMap is a separate map from our default map, we need to set its GameMode Override to match. Otherwise, some of our code will not run properly.

  1. Ensure you are still in the ServerMap.
  2. In the World Settings panel, set the GameMode Override and Player Controller Class to match the settings of your Game Default Map.

Next steps #

We’ve built the necessary pieces, so let’s see our work in action. In the next section, we’ll tie everything together by getting our tokens, packaging our game server, and configuring the Local Process Capacity Provider. Then we’ll be able to see our game clients connect to the game server immediately after matchmaking concludes.