Build 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 the TutorialGameModeBase.h and TutorialGameModeBase.cpp files defined in the Unreal: Setup tutorial.

Set up the header file #

  1. In your TutorialGameModeBase.h header file, ensure that your includes section looks like the following:

    #include "CoreMinimal.h"
    #include "GameFramework/GameModeBase.h"
    #include "GameFramework/Actor.h"
    #include "PragmaGameServer.h"
    #include "TutorialGameModeBase.generated.h"
    
  2. Declare the public and protected functions

    In the header file, declare the EndGame method, as well as the methods that will handle game-related events (HandleGameStartDataReceived(), HandlePlayerConnected(), HandleConnectPlayersResponse(), and HandleEndGameResponse()):

    public:
    	void EndGame();
    
    protected:
    	void HandleGameStartDataReceived(FPragma_GameInstance_GameStart GameStartData);
    	void HandlePlayerConnected(APlayerController* NewPlayer) const;
    	void HandleConnectPlayersResponse(TPragmaResult<FPragma_GameInstance_ConnectPlayersV1Response> Result, const FPragmaMessageMetadata& Metadata);
    	void HandleEndGameResponse(TPragmaResult<FPragma_GameInstance_EndGameV1Response> Result, const FPragmaMessageMetadata& Metadata) const;
    

Build TutorialGameModeBase functionality #

Define BeginPlay() #

Execution of TutorialGameModeBase starts from the BeginPlay() function. This code instantiates UPragmaGameServer, which we built in the previous section of this guide, and calls its Start() function. The method also registers several event handlers that we declared in the header file. We’ll define these methods in the next step.

In your TutorialGameModeBase.cpp source file, ensure that BeginPlay() contains the following code:

// Called when the game starts or when spawned
void ATutorialGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	if (!GetWorld()->IsNetMode(NM_DedicatedServer))
	{
		return;
	}
	
	GameServer = NewObject<UPragmaGameServer>(this);
	GameServer->OnGameStartDataReceived.AddUObject(this, &ATutorialGameModeBase::HandleGameStartDataReceived);
	GameServer->Start();

	auto* GameMode = Cast<ATutorialGameModeBase>(GetWorld()->GetAuthGameMode());
	GameMode->OnPlayerConnected.AddUObject(this, &ATutorialGameModeBase::HandlePlayerConnected);
	GameMode->OnGameEnded.AddUObject(this, &ATutorialGameModeBase::EndGame);

}

Define handler functions #

BeginPlay() uses the following methods to handle game-related events.

  1. 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)
    	);
    }
    

    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.

  2. Define HandleConnectPlayersResponse():

    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.

  3. Define HandlePlayerConnected():

    void ATutorialsGameModeBase::HandlePlayerConnected(APlayerController* NewPlayer) const
    {
    	SERVER_LOG("Player connected!");
    	Cast<ATutorialPlayerController>(NewPlayer)->ClientNotifyConnected();
    }
    
  4. Define HandleEndGameResponse:

    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();
    }
    

Define EndGame() #

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.

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();
	}
}

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);
    
  3. 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);
    }
    
  4. In the player controller’s Init() function, append the following line to the end to register HandleConnectionDetailsReady() to the OnHostConnectionDetailsReceived event:

    Player->GameLoopApi().OnHostConnectionDetailsReceived.AddUObject(this, &ATutorialPlayerController::HandleConnectionDetailsReady);
    
  5. 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.

  6. Also declare ClientNotifyConnected():

    public:
    	UFUNCTION(Client, Reliable)
    	void ClientNotifyConnected();
    
  7. 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.