Unreal: Matchmaking #

This tutorial uses Unreal Engine 5.3 with Pragma Engine 0.0.101 to demonstrate integrating Pragma Engine party functionality with a third-party game engine. This guide assumes you are proficient with Unreal Editor.

We’ve previously built the party flow up until the point players enter matchmaking: players can create a new party, join a party with an invite code, and select their game mode and characters. In this tutorial, we’ll expand the MyPlayerController.h header file and MyPlayerController.cpp source file to implement matchmaking functionality.

Prerequisites

Required:

Get started #

To get started, re-run the update scrips command:

update-pragma-sdk.sh

Ensure you have a locally running Pragma Engine to test examples as you build each function.

How to use this tutorial #

The code presented in this tutorial is simplified to give you an introduction to the game flow. An actual game would have a more sophisticated design, and the player experience may differ significantly.

We’ve built this barebones matchmaking screen to help you visualize the functionality presented in this tutorial:

Screenshot of an example matchmaking screen in the Unreal game client

The functions in this tutorial are built as UFunctions with the Exec and BlueprintCallable specifiers, meaning they can be executed by the in-game console and in a Blueprint or Level Blueprint graph. The Exec specifier is useful for quickly testing your functions.

The example tests in each section are meant to ensure your C++ code is working properly and are unlikely to represent a completed game design. Adapt the organization and naming to suit your project’s needs.

For convenience, we’ve included sample C++ files that contain all the code from this tutorial, as well as the login/logout functionality and the party functionality.

Note that you may need to update your #include statements as you progress through the tutorial.

Enter matchmaking #

Goal #

Implement a EnterMatchmaking function that allows players in a party to enter matchmaking.

Steps #

  1. Declare the EnterMatchmaking() function in the public section of your MyPlayerController.h file:

    UFUNCTION(Exec, BlueprintCallable, meta=(Category="Pragma"))
    void EnterMatchmaking();
    
  2. Define EnterMatchmaking() in your MyPlayerController.cpp file to enter the party into a matchmaking queue:

    void AMyPlayerController::EnterMatchmaking()
    {
    	UPragmaGameLoopApi::FOnCompleteDelegate OnEnterMatchmakingDelegate;
    	OnEnterMatchmakingDelegate.BindWeakLambda(this, [this](TPragmaResult<> Result)
    	{
    		if (Result.IsSuccessful())
    		{
    			UE_LOG(LogTemp, Display, TEXT("Enter matchmaking success."));
    		}
    		else
    		{
    			UE_LOG(LogTemp, Warning, TEXT("Pragma unable to enter matchmaking: %s"), *Result.Error().ToString());
    		}
    	});
    
    	Player->GameLoopApi().EnterMatchmaking(OnEnterMatchmakingDelegate);
    }
    

As long as the two players are in different parties and both parties are set to the same game mode, the WarmbodyMatchmakingPlugin will match the two parties, create a game instance, and send the two parties to the game instance.

Players cannot enter matchmaking without being in the “ready” state. A player’s isReady value is automatically set to true when they make a character selection. See Make character selections.

Display game instance information #

Goal #

Create an Unreal function that logs to the console whenever a player has been added to a game instance. This will verify the matchmaking process has succeeded.

The Pragma SDK provides an OnAddedToGame event that fires when a party is added to a game instance. We can use these events in Unreal to trigger handler functions for matchmaking and game instance updates. For now, our HandleOnAddedToGame() function simply print an Unreal log entry.

Steps #

  1. Declare the HandleOnAddedToGame() function in your MyPlayerController.h file under private:

    void HandleOnAddedToGame(const FString GameInstanceId, const FPragma_GameInstance_ExtAddedToGame Ext);
    
  2. Define HandleOnAddedToGame() in your MyPlayerController.cpp file:

    void AMyPlayerController::HandleOnAddedToGame(const FString GameInstanceId, const FPragma_GameInstance_ExtAddedToGame Ext)
    {
    	UE_LOG(LogTemp, Display, TEXT("Game instance ID: %s"), *GameInstanceId );
    }
    
  3. Register our HandleOnAddedToGame() function with the appropriate event handler in the MyPlayerController.cpp file in the BeginPlay() function:

    Player->GameLoopApi().OnAddedToGame.AddUObject(
    	this, &AMyPlayerController::HandleOnAddedToGame);
    

Test matchmaking functionality #

To test this functionality using the Unreal in-game console:

  1. Open two clients and log in as test01 and test02.

  2. As test01, create a party and set the game mode to Casual. Set a character.

  3. As test02 , create a party and set the game mode to Casual. Set a character.

  4. As test01, call EnterMatchmaking()

  5. As test02, call EnterMatchmaking()

To apply this functionality using Unreal Blueprints, create an “Enter matchmaking” button that calls the EnterMatchmaking function and populated a text box with the resulting game instance ID.

Upon successfully entering matchmaking, the Unreal output log should display a “Enter matchmaking success.” message for each player, as well as identical game instance IDs for each player.

Sample header and source files #

The following sample files combine the code blocks from this tutorial, along with the functions from the Handle Login and Logout tutorial and Unreal: Parties tutorial.

Sample MyPlayerController.h header file
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "PragmaPtr.h"
#include "PragmaGameInstanceSubsystem.h"
#include "PragmaResult.h"
#include "Dto/PragmaGameInstanceExtDto.h"
#include "Dto/PragmaPartyRpcExtDto.h"
#include "Services/Party/PragmaParty.h"
#include "PragmaLocalPlayerSubsystem.h"
#include "MyPlayerController.generated.h"


// Forward declares Pragma pointer types for Pragma::FPlayer.
PRAGMA_FWD(FPlayer);

UCLASS()
class UNREALTUTORIAL_API AMyPlayerController : public APlayerController
{
	GENERATED_BODY()

public:
    
	virtual void BeginPlay() override;

	// Weak pointer to our Pragma Player owned by the PragmaLocalPlayerSubsystem.
	Pragma::FRuntimePtr Runtime;
	Pragma::FPlayerPtr Player;

	//Login and Logout functions
	
	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	void LogIn(const FString& Username);

	UFUNCTION(Exec, BlueprintCallable, meta=(Category="Pragma"))
	void LogOut();

	//Initialization functions

	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	void InitializeGameLoop();
		
	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	void InitializeSocial();
	
	//Party functions
	
	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	void CreateParty();
	
	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	void JoinPartyWithCode(const FString& InviteCode);
	
	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	void SendPartyInviteByPlayerId(const FString& InviteeId);

	UFUNCTION(Exec, BlueprintCallable, meta=(Category="Pragma"))
	void RespondToPartyInvite(const FString& PartyInviteId, const bool response);

	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	void SetGameMode(const EPragma_Party_GameMode& GameModeSelection);

	UFUNCTION(Exec, BlueprintCallable, meta=(Category="Pragma"))
	void SetCharacter(const EPragma_Party_Character& CharacterSelection);

	UFUNCTION(Exec, BlueprintCallable, meta=(Category="Pragma"))
	void LeaveParty();
	
	//Matchmaking functions
	UFUNCTION(Exec, BlueprintCallable, meta=(Category="Pragma"))
	void EnterMatchmaking();


	//Get functions

	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	FString GetFullUsername();

	UFUNCTION(Exec, BlueprintCallable, meta=(Category="Pragma"))
	FString GetPlayerId();

	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	FString GetSocialId();

	UFUNCTION(Exec, BlueprintCallable, Category="Pragma")
	TArray<FString> GetPartyPlayers();

	UFUNCTION(Exec, BlueprintCallable, meta=(Category="Pragma"))
	TArray<FString> GetPartyInvites();
	
	
private:
	void HandleLoggedIn(const TPragmaResult<>& Result);
	void HandleLoggedOut();
	void HandleOnAddedToGame(const FString GameInstanceId, const FPragma_GameInstance_ExtAddedToGame Ext);
};
Sample MyPlayerController.cpp source file
// Fill out your copyright notice in the Description page of Project Settings.

#include "MyPlayerController.h"

#include "PragmaPlayer.h"
#include "Services/Party/PragmaParty.h"
#include "Dto/PragmaAccountRpcDto.h"
#include "Dto/PragmaPartyRpcExtDto.h"
#include "Dto/PragmaPresenceExtDto.h"
#include "PragmaLocalPlayerSubsystem.h"

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

	UE_LOG(LogTemp, Display, TEXT("Initializing") );
	
	const auto* Subsystem = GetLocalPlayer()->GetSubsystem<UPragmaLocalPlayerSubsystem>();

	// The Pragma Runtime is automatically initialized in the PragmaGameInstanceSubsystem.
	auto RuntimeA = Subsystem->Runtime();

	// Set configuration for the SDK before logging in.
	RuntimeA->Config().BackendAddress = "http://127.0.0.1:10000";
	RuntimeA->Config().ProtocolType = EPragmaProtocolType::WebSocket;
	RuntimeA->Config().GameClientVersion = "GameServerVersion1";

	// The UPragmaLocalPlayerSubsystem is automatically initialized
	// with a Pragma Player object for every LocalPlayer.
	Player = Subsystem->Player();

	Player->GameLoopApi().OnAddedToGame.AddUObject(
	this, &AMyPlayerController::HandleOnAddedToGame);
	
}

//Login and Logout functions

void AMyPlayerController::LogIn(const FString& Username)
{
    Player->LogIn(
        EPragma_Account_IdProvider::UNSAFE,
        Username,
        Pragma::FPlayer::FLoggedInDelegate::CreateUObject(
            this, &AMyPlayerController::HandleLoggedIn));
}

void AMyPlayerController::LogOut()
{
    Player->LogOut(Pragma::FPlayer::FLoggedOutDelegate::CreateUObject(
        this, &AMyPlayerController::HandleLoggedOut));
}

//Initialization functions

void AMyPlayerController::InitializeGameLoop()
{
	UPragmaGameLoopApi::FOnCompleteDelegate GameLoopInitializeDelegate;
	GameLoopInitializeDelegate.BindWeakLambda(this, [this](const TPragmaResult<>& Result)
		{
			if (Result.IsSuccessful())
			{
				UE_LOG(LogTemp, Display, TEXT("Game loop services initialized."));
			}
			else
			{
				UE_LOG(LogTemp, Warning, TEXT("Game loop services failed to initialize: %s"), *Result.Error().ToString());
			}
		});
	Player->GameLoopApi().Initialize(GameLoopInitializeDelegate);
}


//Party functions

void AMyPlayerController::CreateParty()
{
	UPragmaGameLoopApi::FOnCompleteDelegate OnPartyCreatedDelegate;
	OnPartyCreatedDelegate.BindWeakLambda(this, [this](TPragmaResult<> Result)
	{
		if (Result.IsSuccessful())
		{
			UE_LOG(LogTemp, Display, TEXT("Pragma party created with code: %s"), *Player->GameLoopApi().GetPartyState()->Party()->GetInviteCode());
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("Pragma unable to create party: %s"), *Result.Error().ToString());
		}
	});

	Player->GameLoopApi().CreateParty(
		FPragma_Party_ExtCreateRequest{},
		FPragma_Party_ExtPlayerJoinRequest{},
		TArray<FString>(),
		TMap<FString, int32>(),
		OnPartyCreatedDelegate);
}

void AMyPlayerController::JoinPartyWithCode(const FString& InviteCode)
{
	UPragmaGameLoopApi::FOnCompleteDelegate JoinWithInviteCodeDelegate;
	JoinWithInviteCodeDelegate.BindWeakLambda(this, [=, this](TPragmaResult<> Result)
	{
		if (Result.IsSuccessful())
		{
			UE_LOG(LogTemp, Display, TEXT("Joined party using invite code %s"), *InviteCode);
		}
		else
		{
			UE_LOG(LogTemp, Warning,
				TEXT("Unable to join party: %s"), *Result.Error().ToString());
		}
	});

	Player->GameLoopApi().JoinPartyWithInviteCode(
		FPragma_Party_ExtPlayerJoinRequest{},
		InviteCode,
		JoinWithInviteCodeDelegate
	);
}

void AMyPlayerController::SendPartyInviteByPlayerId(const FString& InviteeId)
{
	Player->GameLoopApi().SendPartyInvite(
		InviteeId,
		UPragmaGameLoopApi::FOnInviteSentDelegate::CreateWeakLambda(
	this, [this, InviteeId](const TPragmaResult<FString>& SendPartyInviteResult)
	{
		if (SendPartyInviteResult.IsSuccessful())
		{
			const FString InviteId = SendPartyInviteResult.Payload();
			UE_LOG(LogTemp, Display, TEXT("Send party invite by player id succeeded. Party invite ID: %s"), *InviteId);
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("Pragma unable to send invite: %s"), *SendPartyInviteResult.Error().ToString());
		}
	})
	);
}

TArray<FString> AMyPlayerController::GetPartyInvites()
{
	const TArray<FPragmaPartyInvite> PartyInvites = Player->GameLoopApi().GetPendingPartyInvites();

	TArray<FString> PartyInviteCodes;

	for(const FPragmaPartyInvite PartyInvite : PartyInvites)
	{
		FString PartyInviteCode = PartyInvite.GetInviteId();
		UE_LOG(LogTemp, Display, TEXT("Party invite id: %s"), *PartyInviteCode);
		PartyInviteCodes.Add(PartyInviteCode);
	}

	return PartyInviteCodes;
}

void AMyPlayerController::RespondToPartyInvite(const FString& PartyInviteCode, const bool Response)
{
	UPragmaGameLoopApi::FOnCompleteDelegate RespondInviteDelegate;
	RespondInviteDelegate.BindWeakLambda(this, [=, this](TPragmaResult<> Result)
	{
		if (Result.IsSuccessful())
		{
			if (Response==true)
			{
				UE_LOG(LogTemp, Display, TEXT("Accepted party invite id %s. Party successfully joined."), *PartyInviteCode);
			}
			else
			{
				UE_LOG(LogTemp, Display, TEXT("Declined party invite id %s"), *PartyInviteCode);
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning,
				TEXT("Unable to respond: %s"), *Result.Error().ToString());
		}
	});

	Player->GameLoopApi().RespondToPartyInvite(
		FPragma_Party_ExtPlayerJoinRequest{},
		PartyInviteCode,
		Response,
		RespondInviteDelegate
	);
}

void AMyPlayerController::SetGameMode(const EPragma_Party_GameMode& GameModeSelection)
{
	if(Player->GameLoopApi().IsLeaderOfParty(Player->Id())==true)
	{
		FPragma_Party_ExtUpdatePartyRequest Request;
		Request.Update.SetNewGameMode(GameModeSelection);

		UPragmaGameLoopApi::FOnCompleteDelegate UpdatePartyDelegate;
	
		UpdatePartyDelegate.BindWeakLambda(this, [=, this](TPragmaResult<> Result)
		{
			if (Result.IsSuccessful())
			{
				UE_LOG(LogTemp, Display,
					TEXT("Changed game mode selection to %s"), *UEnum::GetValueAsString<EPragma_Party_GameMode>(GameModeSelection));
			}
			else
			{
				UE_LOG(LogTemp, Warning,
					TEXT("Unable to change game mode: %s"), *Result.Error().ToString());
			}
		});

		Player->GameLoopApi().UpdateParty(Request, UpdatePartyDelegate);
	}
	else
	{
		UE_LOG(LogTemp, Display,TEXT("Only party leaders can select game mode"));
	}
}

void AMyPlayerController::SetCharacter(const EPragma_Party_Character& CharacterSelection)
{
	UPragmaGameLoopApi::FOnCompleteDelegate UpdatePartyPlayerDelegate;
	UpdatePartyPlayerDelegate.BindWeakLambda(this, [=, this](TPragmaResult<> Result)
	{
		if (Result.IsSuccessful())
		{
			UE_LOG(LogTemp, Display,
				TEXT("Changed character selection to %s"), *UEnum::GetValueAsString<EPragma_Party_Character>(CharacterSelection));
		}
		else
		{
			UE_LOG(LogTemp, Warning,
				TEXT("Unable to change character: %s"), *Result.Error().ToString());
		}
	});

	FPragma_Party_ExtUpdatePartyPlayerRequest Request;
	Request.Update.SetNewCharacter(CharacterSelection);

	Player->GameLoopApi().UpdatePartyPlayer(Request, UpdatePartyPlayerDelegate);
}

void AMyPlayerController::LeaveParty()
{
	UPragmaGameLoopApi::FOnCompleteDelegate LeavePartyDelegate;
	LeavePartyDelegate.BindWeakLambda(this, [this](TPragmaResult<> Result)
	{
		if (Result.IsSuccessful())
		{
			UE_LOG(LogTemp, Display, TEXT("Successfully left party."));
		}
		else
		{
			UE_LOG(LogTemp, Warning,
				TEXT("Unable to leave party: %s"), *Result.Error().ToString());
		}
	});
	Player->GameLoopApi().LeaveParty(LeavePartyDelegate);
}

//Matchmaking functions

void AMyPlayerController::EnterMatchmaking()
{
	UPragmaGameLoopApi::FOnCompleteDelegate OnEnterMatchmakingDelegate;
	OnEnterMatchmakingDelegate.BindWeakLambda(this, [this](TPragmaResult<> Result)
	{
		if (Result.IsSuccessful())
		{
			UE_LOG(LogTemp, Display, TEXT("Enter matchmaking success."));
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("Pragma unable to enter matchmaking: %s"), *Result.Error().ToString());
		}
	});

	Player->GameLoopApi().EnterMatchmaking(OnEnterMatchmakingDelegate);
}



//Handler functions

void AMyPlayerController::HandleLoggedIn(const TPragmaResult<>& Result)
{
    if (Result.IsFailure())
    {
        UE_LOG(LogTemp, Display, TEXT("Pragma -- Login failed: %s"), *Result.ErrorCode());
        return;
    }
    UE_LOG(LogTemp, Display, TEXT("Pragma -- Logged in."));
}

void AMyPlayerController::HandleLoggedOut()
{
    UE_LOG(LogTemp, Display, TEXT("Pragma -- Logged out."));
}

void AMyPlayerController::HandleOnAddedToGame(const FString GameInstanceId, const FPragma_GameInstance_ExtAddedToGame Ext)
{
	UE_LOG(LogTemp, Display, TEXT("Game instance ID: %s"), *GameInstanceId );
}



//Get functions

//Get player's username (display name and discriminator)
FString AMyPlayerController::GetFullUsername()
{
	UE_LOG(LogTemp, Display, TEXT("Your full username: %s"), *Player->FullDisplayName());
	return Player->FullDisplayName();
}

//Get player's UUID
FString AMyPlayerController::GetPlayerId()
{
	UE_LOG(LogTemp, Display, TEXT("Your player ID: %s"), *Player->Id());
	return Player->Id();
}

FString AMyPlayerController::GetSocialId()
{
	UE_LOG(LogTemp, Display, TEXT("Your social ID: %s"), *Player->SocialId());
	return Player->SocialId();
}


//Get list of player objects for players in the party
TArray<FString> AMyPlayerController::GetPartyPlayers()
{
	
	const TArray<const UPragmaPartyPlayer*> PartyPlayers = Player->GameLoopApi().GetPartyState()->Party()->GetPlayers();
	
	TArray<FString> DisplayNames;

	const FString& YourPlayerId = Player->Id();

	for (const UPragmaPartyPlayer* PartyPlayer : PartyPlayers)
	{
		FString DisplayName = PartyPlayer->GetDisplayName().DisplayName;

		FString PlayerId = PartyPlayer->GetPlayerId();

		FString SocialId = PartyPlayer->GetSocialId();

		if (PlayerId == YourPlayerId)
		{
			DisplayName += " (You)";
		}

		if (PartyPlayer->IsLeader())
		{
			DisplayName += " (Leader)";
		}

		if (PartyPlayer->IsReady())
		{
			DisplayName += " (Ready)";
		}

		DisplayNames.Add(DisplayName);

		UE_LOG(LogTemp, Display, TEXT("Display name: %s, Player ID: %s, Social ID: %s"), *DisplayName, *PlayerId, *SocialId);
	}

	return DisplayNames;
}

Next steps #

At this point, players can enter matchmaking and enter game instances with appropriate matches. Continue to the Social tutorial to learn how to implement sample friend and presence functionality.