Skip to main content

Implementing the subsystem - Game client integration - (Unreal Engine module)

Last updated on March 12, 2025

About the sample matchmaking backend service

Before you begin, take a look at the communication between game client and the sample matchmaking backend service server to understand the implementation better.

Unwrap the subsystem

To follow this tutorial, a Game Instance Subsystem called CustomMatchmakingSubsystem_Starter has been prepared for you. This class is available in the Resources section and consists of the following files:

  • Header file: /Source/AccelByteWars/TutorialModules/Play/CustomMatchmaking/CustomMatchmakingSubsystem_Starter.cpp
  • CPP file: /Source/AccelByteWars/TutorialModules/Play/CustomMatchmaking/CustomMatchmakingSubsystem_Starter.h

The CustomMatchmakingSubsystem_Starter class has several functionalities provided.

  • Delegates that will later be used to bind functions to the matchmaking events.
public:
FOnMatchmakingStarted OnMatchmakingStartedDelegates;
FOnMatchmakingStopped OnMatchmakingStoppedDelegates;
FOnMatchmakingError OnMatchmakingErrorDelegates;
FOnMatchmakingMessageReceived OnMatchmakingMessageReceivedDelegates;
  • WebSocket reference. Byte Wars uses Unreal Engine's built in WebSocket handler for this module.
private:
TSharedPtr<IWebSocket> WebSocket;
  • A setup WebSocket function. The actual WebSocket setup is not yet present in this starter file, the code that you can see in the CPP file is the logic to retrieve the server URL from launch argument or config file.
private:
// ...
void SetupWebSocket();
  • A cleanup WebSocket function. A dummy function that you will later use to cleanup the WebSocket.
private:
// ...
void CleanupWebSocket();
  • A variable to store custom error message. This is to make the error more user-friendly. Passing a custom error message when closing the WebSocket doesn't make the coresponding delegate passes the same error message. Hence there is this variable.
private:
// ...
FString PendingDisconnectReason = TEXT("");
  • A function to throw a payload invalid error. This is to handle the cases where the matchmaking service send a payload that is not what the game expect. The game will then close the WebSocket connection.
void UCustomMatchmakingSubsystem_Starter::ThrowInvalidPayloadError()
{
UE_LOG_CUSTOMMATCHMAKING(Warning, TEXT("Unexpected message format was received from the Matchmaking service, closing websocket"))
PendingDisconnectReason = TEXT_WEBSOCKET_PARSE_ERROR;
WebSocket->Close(WEBSOCKET_ERROR_CODE_UNEXPECTED_MESSAGE, TEXT_WEBSOCKET_PARSE_ERROR);
}

Beside the starter subsystem, also prepared are some constants, delegate declaration, struct declaration, and helpers in the /Source/AccelByteWars/TutorialModules/Play/CustomMatchmaking/CustomMatchmakingModels.h file.

  • Text and string constants.
#define WEBSOCKET_ERROR_CODE_UNEXPECTED_MESSAGE (int) 4001

#define WEBSOCKET_FAILED_GENERIC_MESSAGE FString(TEXT("connect failed"))

#define CUSTOM_MATCHMAKING_CONFIG_SECTION_URL FString(TEXT("CustomMatchmaking"))
#define CUSTOM_MATCHMAKING_CONFIG_KEY_URL FString(TEXT("CustomMatchmakingUrl"))
#define DEFAULT_MATCHMAKING_CONFIG_URL TEXT("ws://127.0.0.1:8080")
#define WEBSOCKET_PROTOCOL TEXT("ws")
#define TEXT_LOADING_TRAVELLING FString(TEXT("Travelling to server"))
#define TEXT_LOADING_FINDING_MATCH FString(TEXT("Finding Match"))
#define TEXT_LOADING_REQUEST FString(TEXT("Requesting"))
#define TEXT_LOADING_CANCEL FString(TEXT("Canceling"))
#define TEXT_ERROR_CANCELED FString(TEXT("Canceled"))
#define TEXT_WEBSOCKET_ERROR_GENERIC FString(TEXT("Connect failed.\nMake sure the Matchmaking server is running, reachable, and the address and port is set properly"))
#define TEXT_WEBSOCKET_PARSE_ERROR FString(TEXT("Received invalid payload format from Matchmaking server. Make sure you are running a compatible version."))
  • Delegate declarations.
DECLARE_MULTICAST_DELEGATE(FOnMatchmakingStarted)
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMatchmakingStopped, const FString& /*Reason*/)
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMatchmakingMessageReceived, const FMatchmakerPayload& /*Payload*/)
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMatchmakingError, const FString& /*ErrorMessage*/)
  • Struct and enum representing the message received from the sample matchmaking backend service. USTRUCT and UPROPERTY are used to take advantage of Unreal Engine built in function to convert JSON to struct, the FJsonObjectConverter::JsonObjectStringToUStruct function.
UENUM(BlueprintType)
enum class EMatchmakerPayloadType : uint8
{
OnFindingMatch,
OnMatchFound,
OnServerClaimFailed,
OnServerReady
};

USTRUCT()
struct FMatchmakerPayload
{
GENERATED_BODY()

public:
UPROPERTY()
EMatchmakerPayloadType Type;

UPROPERTY()
FString Message;
};

Implement matchmaking

  1. Open the CustomMatchmakingSubsystem_Starter Header file and add the following function declarations. UE's WebSocket interface has four events: OnConnected, OnClosed, OnMessage, and OnConnectionError. The functions declared here will handle what happens when each of these events is triggered.
private:
// ...
UFUNCTION()
void OnConnected() const;

UFUNCTION()
void OnClosed(int32 StatusCode, const FString& Reason, bool WasClean);

UFUNCTION()
void OnMessage(const FString& Message);

UFUNCTION()
void OnError(const FString& Error) const;
  1. Open the CustomMatchmakingSubsystem_Starter CPP file and define the OnConnected function. This function will handle what happens after a connection was succesfully established. In this case, the game will simply trigger a delegate to let other object do something.
void UCustomMatchmakingSubsystem_Starter::OnConnected() const
{
UE_LOG_CUSTOMMATCHMAKING(Verbose, TEXT("Websocket connected"))
OnMatchmakingStartedDelegates.Broadcast();
}
  1. Define the OnClosed function, which will handle what happens after a connection is closed. Here, we will call the CleanupWebSocket function.
void UCustomMatchmakingSubsystem_Starter::OnClosed(int32 StatusCode, const FString& Reason, bool WasClean)
{
UE_LOG_CUSTOMMATCHMAKING(Verbose, TEXT("Websocket closed: (%d) %s"), StatusCode, *Reason)

// Modify the error message to make it more user-friendly
const FString ModifiedReason = PendingDisconnectReason.IsEmpty() ? TEXT_WEBSOCKET_ERROR_GENERIC : PendingDisconnectReason;

OnMatchmakingStoppedDelegates.Broadcast(ModifiedReason);
CleanupWebSocket();
}
  1. Define the OnMessage function. This function will be called when the game receives any messages from the matchmaking service. As shown in the sample matchmaking backend service diagram, you'll see that the matchmaking server sends messages as JSON-formatted strings with four possible type value: OnFindingMatch, OnMatchFound, OnServerReady, and OnServerClaimFailed. For most message types, the game simply triggers a delegate and passes the message. However, for OnServerReady, in addition to triggering a delegate, the game also attempts to connect to the IP address provided in the message.
void UCustomMatchmakingSubsystem_Starter::OnMessage(const FString& Message)
{
UE_LOG_CUSTOMMATCHMAKING(Verbose, TEXT("Websocket message received: %s"), *Message)
if (Message.IsEmpty())
{
UE_LOG_CUSTOMMATCHMAKING(VeryVerbose, TEXT("Message is empty, skipping"))
return;
}

// Parse message
FMatchmakerPayload Payload;
// Safety in case the received message is different than expected
if (!FJsonObjectConverter::JsonObjectStringToUStruct<FMatchmakerPayload>(Message, &Payload))
{
ThrowInvalidPayloadError();
return;
}

// Notify
OnMatchmakingMessageReceivedDelegates.Broadcast(Payload);

// Travel if server is ready
if (Payload.Type == EMatchmakerPayloadType::OnServerReady)
{
// Use FInternetAddr to check whether this is a valid IPv4 or IPv6
const TSharedPtr<FInternetAddr> ServerAddress = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr();
bool bIsValid = false;

ServerAddress->SetIp(*Payload.Message, bIsValid);
if (!bIsValid)
{
ThrowInvalidPayloadError();
return;
}

// Trigger travel
AAccelByteWarsPlayerController* PC = Cast<AAccelByteWarsPlayerController>(GetWorld()->GetFirstPlayerController());
if (!PC)
{
UE_LOG_CUSTOMMATCHMAKING(Warning, TEXT("Player Controller is not AAccelByteWarsPlayerController, cancelling travel"))
return;
}

const FString Address = ServerAddress->ToString(true);
PC->DelayedClientTravel(Address, ETravelType::TRAVEL_Absolute);
}
}
  1. Define the OnError funcion which will handle what happens when the WebSocket failed. Here, the game simply triggers delegate to let other object do something.
void UCustomMatchmakingSubsystem_Starter::OnError(const FString& Error) const
{
UE_LOG_CUSTOMMATCHMAKING(Verbose, TEXT("Websocket error: %s"), *Error)

// Use a generic message since UE's built in error message are not clear enough
OnMatchmakingErrorDelegates.Broadcast(TEXT_WEBSOCKET_ERROR_GENERIC);
}
  1. Open the CustomMatchmakingSubsystem_Starter Header file and add the following function declarations.
public:
// ...
void StartMatchmaking();
void StopMatchmaking();
  1. Open the CustomMatchmakingSubsystem_Starter CPP file and define the StartMatchmaking function. Refering back to sample matchmaking backend service diagram, to start matchmake the game need to simply connect to the server via WebSocket.
void UCustomMatchmakingSubsystem_Starter::StartMatchmaking()
{
// In this sample, connecting to the websocket will immediately start the matchmaking
SetupWebSocket();
WebSocket->Connect();
}
  1. Go to the SetupWebSocket function and add the following code to the end of the function. Here, we're doing two things, construct the WebSocket, and connect our functions to the WebSocket events.
void UCustomMatchmakingSubsystem_Starter::SetupWebSocket()
{
// ...
// WebSocket setup
UE_LOG_CUSTOMMATCHMAKING(Verbose, TEXT("WebSocket target URL: %s"), *ServerUrl)
WebSocket = FWebSocketsModule::Get().CreateWebSocket(ServerUrl, WEBSOCKET_PROTOCOL);

// Bind events
WebSocket->OnConnected().AddUObject(this, &ThisClass::OnConnected);
WebSocket->OnConnectionError().AddUObject(this, &ThisClass::OnError);
WebSocket->OnClosed().AddUObject(this, &ThisClass::OnClosed);
WebSocket->OnMessage().AddUObject(this, &ThisClass::OnMessage);
}
  1. Define the StopMatchmaking function. Stopping matchmake in this sample matchmaking backend service is done simply by closing the WebSocket connection.
void UCustomMatchmakingSubsystem_Starter::StopMatchmaking()
{
// Cancel the matchmaking by disconnecting from the websocket
if (!WebSocket)
{
UE_LOG_CUSTOMMATCHMAKING(Verbose, TEXT("Websocket is null, operation cancelled"))
return;
}

PendingDisconnectReason = TEXT_ERROR_CANCELED;
WebSocket->Close();
}
  1. Go to CleanupWebSocket function and add the following code. This function will clean up the current WebSocket object, unbinding all the events, and destroying the reference.
void UCustomMatchmakingSubsystem_Starter::CleanupWebSocket()
{
PendingDisconnectReason = TEXT("");

// Unbind events
WebSocket->OnConnected().RemoveAll(this);
WebSocket->OnConnectionError().RemoveAll(this);
WebSocket->OnClosed().RemoveAll(this);
WebSocket->OnMessage().RemoveAll(this);

WebSocket = nullptr;
}

Resources