Skip to main content

Configure P2P

Last updated on October 24, 2024

Introduction

AccelByte Gaming Services (AGS) supports Peer-to-peer (P2P) networks for your game. P2P gaming leverages decentralized networking architecture, enabling players to directly connect with each other without relying on centralized servers. This approach offers numerous benefits, including reduced latency, improved scalability, and enhanced player autonomy.

By distributing the game's workload across participating peers, P2P gaming can alleviate strain on server infrastructure, resulting in smoother gameplay experiences and decreased likelihood of server downtime. Additionally, P2P networks facilitate player-to-player interactions, fostering a sense of community and enabling seamless multiplayer experiences across various platforms.

This article walks you through how to configure P2P for your game using AGS online subsystem (OSS) or Unity SDK.

How it works

This section contains diagrams showing how P2P will work in AGS using AGS OSS or Unity SDK.

Host a game

Client join a game

Set up Matchmaking configuration in the Admin Portal

  1. Create session template. Follow the steps in the Configure session templates article to configure session templates. Then, make sure to choose P2P for the session type as shown below.

    Setting the session type to P2P

  2. Create match ruleset and match pool. Follow the steps in the Configure match ruleset article and in the Configure match pool article. Then, make sure to choose the correct session template with the P2P session type that you created in step 1.

  3. Integrate Matchmaking in game client. To learn more about the matchmaking flow, refer to the Integrate Matchmaking into your game client. The flow is similar to matchmaking with dedicated servers. The difference with P2P is that after the match is found and the game session is created, the invitation is sent to all game session members and the member that accepts first will be the host and the others will be clients.

Set up matchmaking using OSS

To implement matchmaking into your game, follow these steps:

Set up matchmaking

  • Enable Sessions V2 in the AccelByte Online Subsystem (OSS) by adding the following to the DefaultEngine.ini file:

    [OnlineSubsystemAccelByte]
    bEnableV2Sessions=true
    • You will need to enable AccelByteNetworkUtilities by adding the following to the DefaultEngine.ini

      [AccelByteNetworkUtilities]
      UseTurnManager=true
      HostCheckTimeout=5

      [/Script/AccelByteNetworkUtilities.IpNetDriverAccelByte]
      NetConnectionClassName=AccelByteNetworkUtilities.IpConnectionAccelByte
      AllowDownloads=false

      [/Script/Engine.Engine]
      !NetDriverDefinitions=ClearArray
      +NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="AccelByteNetworkUtilities.IpNetDriverAccelByte",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
      +NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver")
  • Add the required permissions for Matchmaking and Session V2 to your Game Client in the Admin Portal.

  • Your Game Client must have authenticated with the AccelByte backend and connected to the Lobby service.

Start matchmaking

In the function where you plan to initiate matchmaking from, you need to acquire the Session Interface from the AccelByte OSS. Note that there is a custom AccelByte Session Interface with the FOnlineSessionAccelBytePtr type that you will be using to request matchmaking, .

const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
if (!ensure(Subsystem != nullptr))
{
return;
}

const FOnlineSessionAccelBytePtr SessionInterface = StaticCastSharedPtr<FOnlineSessionV2AccelByte>(Subsystem->GetSessionInterface());
if (!ensure(SessionInterface.IsValid()))
{
return;
}

Make sure to obtain the Player ID for the Game Client, as you'll need it later to include the Player Controller ID in the matchmaking request. The Player Controller ID will also be used when handling callback results.

const FUniqueNetIdPtr LocalPlayerId = GetUniquePlayerId();
if (!ensure(LocalPlayerId.IsValid()))
{
return false;
}

Create a Session Search Handle FOnlineSessionSearch to be used to store the session result created during the matchmaking process if a match is found successfully. When creating a Session Search Handle, set the query settings with the following parameters:

  • Player Controller ID: the OSS ID of the player for which the match ticket will be submitted during matchmaking.
  • Match Pool Session Settings: the OSS session settings that will be used during matchmaking SETTING_SESSION_MATCHPOOL. By passing this in, it will be updated with the Match Pool Name you provide as the next parameter.
  • Match Pool Name: the name of the match pool you defined as part of the prerequisites for the specified game mode. This will inform the Matchmaking service to add the newly created match ticket to that match pool for evaluation.
  • Comparison Operator: the OSS operator to be used as part of the session search, which in this case is EOnlineComparisonOp::Equals

You need to register a FOnMatchmakingCompleteDelegate callback delegate to listen for the match results, which will trigger when matchmaking completes. Refer to the Sample callback function section for a sample code.

  • Session Name: the name of the game session created if there was a successful match.
  • Success Value: a boolean value indicating whether matchmaking successfully found a match.
// Bind a function that has a return type of void and these parameters:
// FName SessionName, bool bWasSuccessful
const FOnMatchmakingCompleteDelegate OnMatchmakingCompleteDelegate = /* Bind to lambda or class method */;
SessionInterface->AddOnMatchmakingCompleteDelegate_Handle(OnMatchmakingCompleteDelegate);

Once you have the above, you can call StartMatchmaking() from the Session Interface to request matchmaking to start. This function takes the following parameters:

  • Player Controller ID: the OSS ID of the player that the Match Ticket will be submitted for during matchmaking.

  • Session Name: the name of the session created if matchmaking is successful. Typically, you will pass NAME_GameSession.

  • Session Settings: the session settings that should be used by the Session service to create the Game Session, FOnlineSessionSettings(). In this case, you will leave it up to the Session service to assign the settings by passing in an empty object.

  • Session Search Handle: the Session Search Handle you created earlier in this section to store the Game Session that will be created if matchmaking is successful.

Once the request to StartMatchmaking() is made, assuming the call returns true, you should bind the Session Search Handle you defined earlier in this section to a class member for future access. If StartMatchmaking() returns false, that indicates that there was an issue calling into the Matchmaking service.

if (SessionInterface->StartMatchmaking(USER_ID_TO_MATCHMAKING_USER_ARRAY(LocalPlayerId.ToSharedRef()), NAME_GameSession, FOnlineSessionSettings(), MatchmakingSearchHandle, OnStartMatchmakingCompleteDelegate))
{
// Update the current search result handle class member with the search handle passed to the matchmaking service.
CurrentMatchmakingSearchHandle = MatchmakingSearchHandle;
}

Start matchmaking with Turn Server QoS

info

This feature is only available in AGS version 3.75.

The implementation automatically handles the low-level tasks to obtain the Turn Server QoS before creating a match ticket and providing latency information.

To access the custom function of SetIsP2PMatchmaking, first specify a new matchmaking search handle using the FOnlineSessionSearchAccelByte class. Then, set the parameter to true to indicate that the match is for a P2P session. Finally, pass the matchmaking search handle into the StartMatchmaking function to begin the match.

TSharedRef<FOnlineSessionSearchAccelByte> MatchmakingSearchHandle = MakeShared<FOnlineSessionSearchAccelByte>();
MatchmakingSearchHandle->QuerySettings.Set(SETTING_SESSION_MATCHPOOL, MatchPoolName, EOnlineComparisonOp::Equals);
MatchmakingSearchHandle->SetIsP2PMatchmaking(true);

if (SessionInterface_User1->StartMatchmaking(USER_ID_TO_MATCHMAKING_USER_ARRAY(LocalUserId1.ToSharedRef()), NAME_GameSession, FOnlineSessionSettings(), MatchmakingSearchHandle, OnStartMatchmakingCompleteDelegate))
{
CurrentMatchmakingSearchHandle = MatchmakingSearchHandle;
}

Join game session and travel using P2P connection

Once matchmaking completes, the OnMatchmakingCompleteDelegate you bound prior to starting matchmaking will fire and you can process the results by following these instructions.

Retrieve the game session result stored in the SearchResults member array of the Session Search Handle. If the array is empty, refer to Troubleshooting.

// Ensure that we have a valid session search result in the array before continuing
if (!CurrentMatchmakingSearchHandle->SearchResults.IsValidIndex(0))
{
return false;
}

FOnlineSessionSearchResult MatchResult = CurrentMatchmakingSearchHandle->SearchResults[0];
EOnlineSessionTypeAccelByte SessionType = SessionInterface->GetSessionTypeFromSettings(MatchResult.Session.SessionSettings);
if (SessionType != EOnlineSessionTypeAccelByte::GameSession)
{
return false;
}

Check if the player is already in a game session, if so, you will need to destroy it so they can join the new session returned through matchmaking. You can register a FOnDestroySessionCompleteDelegate callback delegate to listen for the result.

// Check if we already have a game session, if so, destroy it to join this one
if (SessionInterface->GetNamedSession(NAME_GameSession) != nullptr)
{
const FOnDestroySessionCompleteDelegate OnDestroySessionForJoinCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnDestroySessionForJoinComplete, Session);
return SessionInterface->DestroySession(NAME_GameSession, OnDestroySessionForJoinCompleteDelegate);
}

Register a FOnJoinSessionCompleteDelegate callback delegate to listen for the join result, which will trigger when the join request completes. In this delegate you will also need to handle P2P connection. You need to check if the user is a game session leader or not. If the user is a game session leader (first user to join game session) then you need to set this user as listen server. If the user is regular member they should initiate connection to game session leader.

// Register a delegate for joining the specified session
const FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnJoinSessionComplete);
JoinSessionDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate);

Here is the sample of OnJoinSessionCompleteDelegate for handling P2P connection

void UOSSDemoGameSessionSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
// Ignore non-game session join results
if (SessionName != NAME_GameSession)
{
return;
}

if (Result != EOnJoinSessionCompleteResult::Success)
{
return;
}

const FOnlineSessionAccelBytePtr SessionInterface = GetSessionInterface();
ensure(SessionInterface.IsValid());

FNamedOnlineSession* Session = SessionInterface->GetNamedSession(SessionName);
if (!ensure(Session != nullptr))
{
return;
}
TSharedPtr<FOnlineSessionInfoAccelByteV2> SessionInfo = StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(Session->SessionInfo);

if (!ensure(SessionInfo.IsValid()))
{
return;
}

ULocalPlayer* LocalPlayer = GetLocalPlayer();
if (!ensure(LocalPlayer != nullptr))
{
return;
}

APlayerController* Controller = LocalPlayer->GetPlayerController(GetWorld());
if (!ensure(Controller != nullptr))
{
return;
}

// If the server type for the created session is either NONE (local), then the connection info is in the session attributes
const EAccelByteV2SessionConfigurationServerType ServerType = SessionInfo->GetServerType();
if (ServerType != EAccelByteV2SessionConfigurationServerType::P2P)
{
// this is not a P2P session
return;
}

FString TravelUrl{};
if (SessionInterface->GetResolvedConnectString(SessionName, TravelUrl, NAME_GamePort) && !TravelUrl.IsEmpty())
{
if(SessionInterface->IsPlayerP2PHost(LocalPlayerId, SessionName))
{
// Travel as listen server
FString MapName;
Session->SessionSettings.Get(SETTING_MAPNAME, MapName);
Controller->ClientTravel(FString::Printf(TEXT("%s?listen"), *MapName), TRAVEL_Absolute);
}
else
{
Controller->ClientTravel(TravelUrl, TRAVEL_Absolute);
}
}
}

Once you have the above, you can call JoinSession() from the Session Interface to join the returned session. This function takes the following parameters:

  • Player Controller ID: the OSS ID of the player for which the match ticket is submitted during matchmaking.

  • Session Name: the name of the game session created if the matchmaking is successful.

  • Session: the OSS Session object to which the the Session service will assign the game session info for later access.

SessionInterface->JoinSession(LocalPlayerId.ToSharedRef().Get(), NAME_GameSession, Session);

Sample callback function

The following is a full example of a callback function that can use as the delegate to listen for OnMatchmakingComplete results:

void OnMatchmakingCompleteDelegate(FName SessionName, bool bWasSuccessful)
{
if (SessionName != NAME_GameSession)
{
return;
}

EOnlineSessionTypeAccelByte SessionType = SessionInterface->GetSessionTypeFromSettings(MatchResult.Session.SessionSettings);
if (SessionType != EOnlineSessionTypeAccelByte::GameSession)
{
return false;
}

// Ensure that we have a valid session search result in the array before continuing
if (!CurrentMatchmakingSearchHandle->SearchResults.IsValidIndex(0))
{
return false;
}

FOnlineSessionSearchResult MatchResult = CurrentMatchmakingSearchHandle->SearchResults[0];

// Check if we already have a game session that we are in, if so, destroy it to join this one
if (SessionInterface->GetNamedSession(NAME_GameSession) != nullptr)
{
const FOnDestroySessionCompleteDelegate OnDestroySessionForJoinCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnDestroySessionForJoinComplete, Session);
return SessionInterface->DestroySession(NAME_GameSession, OnDestroySessionForJoinCompleteDelegate);
}

// Register a delegate for joining the specified session
const FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &UOSSDemoGameSessionSubsystem::OnJoinSessionComplete);


JoinSessionDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate);

const FUniqueNetIdPtr LocalPlayerId = GetUniquePlayerId();
if (!ensure(LocalPlayerId.IsValid()))
{
return false;
}

return SessionInterface->JoinSession(LocalPlayerId.ToSharedRef().Get(), NAME_GameSession, Session);
}
tip

For debugging purposes, use the Relay (TURN) as the Interactive Connectivity Establishment (ICE) connection by passing the additional parameter, -iceforcerelay, during the game launch.

Set up matchmaking using Unity SDK

To implement matchmaking into your game, follow these steps:

Set up matchmaking

  • Make sure you have set up IAM Clients with proper permissions to be used for the game. Follow the steps in the Manage access control for application article.

  • Enable P2P related settings. You can use exact configuration as shown in the image below. We recommend using this configuration. For turn manager server URL, change it to your environment-specific URL. Turn server host, port, username, secret and password are optional. These settings are only used when using static auth key for the turn server. In this example, we use dynamic auth key and tbe mentioned settings are left empty.

AccelByte Configuration screen

Initialize AccelByte network transport and listen to network event

Before initializing the network transport of the game client, you need to log in to AGS and connect to the lobby first. The following snippet code shows how to connect and initialize network transport.

var apiClient = AccelByteSDK.GetClientRegistry().GetApi();
var user = apiClient.GetUser();
var lobby = apiClient.GetLobby();

// instantiate transport manager
AccelByteNetworkTransportManager TransportManager = gameObject.AddComponent<AccelByteNetworkTransportManager>();

// Initialize transport manager after the client is logged in and lobby is connected
lobby.Connected += () =>
{
TransportManager.Initialize(apiClient);
};

// Login and connect lobby
user.LoginWithUsernameV3("username", "password", result =>
{
if (result.IsError)
{
// Handle login error
Debug.Log($"Login Failed: {result.Error.error_description}");
return;
}

// Handle login success
lobby.Connect();
});

If you have Unity Netcode in your scene's singleton, you can pass the component's reference to the NetworkTransport selection.

Network Transport step

Start matchmaking, join game session, and initiate P2P connection

Below is snippet code for start matchmaking

var apiClient = AccelByteSDK.GetClientRegistry().GetApi();
var lobby = apiClient.GetLobby();
var session = apiClient.GetSession();
var matchmaking = apiClient.GetMatchmakingV2();

AccelByteNetworkTransportManager transportManager = gameObject.AddComponent<AccelByteNetworkTransportManager>();

// This notification will be received after the party leader starts the matchmaking
lobby.MatchmakingV2MatchmakingStarted += result =>
{
if (result.IsError)
{
// Handle MatchmakingV2MatchmakingStarted error
Debug.Log($"Error MatchmakingV2MatchmakingStarted, Error Code: {result.Error.Code} Error Message: {result.Error.Message}");
return;
}

// Handle MatchmakingV2MatchmakingStarted success
Debug.Log($"Matchmaking started: {result.Value.ticketId}");
};

// This notification will be received after the match is found
lobby.MatchmakingV2MatchFound += result =>
{
if (result.IsError)
{
// Handle MatchmakingV2MatchFound error
Debug.Log($"Error MatchmakingV2MatchFound, Error Code: {result.Error.Code} Error Message: {result.Error.Message}");
return;
}

// Handle MatchmakingV2MatchFound success
Debug.Log($"Match found: {result.Value.id} {result.Value.matchPool}");
};

// This notification will be received after match is found and game client can accept it
lobby.SessionV2InvitedUserToGameSession += result =>
{
if (result.IsError)
{
// Handle SessionV2InvitedUserToGameSession error
Debug.Log($"Error SessionV2InvitedUserToGameSession, Error Code: {result.Error.Code} Error Message: {result.Error.Message}");
return;
}

// Handle SessionV2InvitedUserToGameSession success
Debug.Log($"Invited to a game session {result.Value.sessionId}");

session.JoinGameSession(result.Value.sessionId, joinGameSessionResult =>
{
if (joinGameSessionResult.IsError)
{
// Handle JoinGameSession error
Debug.Log($"Error joinGameSessionResult, Error Code: {result.Error.Code} Error Message: {result.Error.Message}");
return;
}

// Handle JoinGameSession success
Debug.Log($"Successfully joined game session {joinGameSessionResult.Value.id}");

// Start as a host if the current user is the leader of game session (it means that this user is the first one to join the game)
string gameSessionLeaderId = joinGameSessionResult.Value.leaderId;
if (gameSessionLeaderId == "current-user-member-id")
{
transportManager.StartServer();
}
else
{
transportManager.SetTargetHostUserId(gameSessionLeaderId);
transportManager.StartClient();
}
});
};

// Start matchmaking
var matchpoolName = "matchpool-name";
var optionalParams = new MatchmakingV2CreateTicketRequestOptionalParams();
matchmaking.CreateMatchmakingTicket(matchpoolName, optionalParams, result =>
{
if (result.IsError)
{
// Handle CreateMatchmakingTicket error
Debug.Log($"Error CreateMatchmakingTicket, Error Code: {result.Error.Code} Error Message: {result.Error.Message}");
return;
}

// Handle CreateMatchmakingTicket success
Debug.Log($"Successfully created matchmaking ticket {result.Value.matchTicketId}");
});

Send data to peer without Unity Netcode

To send data to peer, use this method in your game client:

byte[] data = System.Text.Encoding.ASCII.GetBytes("sample data");
ulong peerId = 0;
transportManager.Send(peerId, new ArraySegment<byte>(data), NetworkDelivery.Reliable);

Receive data from peer without Unity Netcode

To receive data from peer, you can set the game client to listen to the previously registered callback for OnTransportEvent, or, alternatively, poll the events using this method:

var networkEvent = transportManager.PollEvent(out ulong clientId, out ArraySegment<byte> payload, out float receiveTime);

Additional functions for Turn Manager in SDK

The Turn Manager API in the AGS SDK supports several utility functions for additional requirements of your game client.

Get TURN servers

This function is called by the game client to retrieve a list of all available TURN servers.

FMultiRegistry::GetApiClient()->TurnManager.GetTurnServersV2(THandler<FAccelByteModelsTurnServerList>::CreateLambda(
[](const FAccelByteModelsTurnServerList& Result)
{
UE_LOG(LogTemp, Log, TEXT("Get Turn Servers Success!"));
}),
FErrorHandler::CreateLambda([](const int32 ErrorCode, const FString& ErrorMessage)
{
UE_LOG(LogTemp, Log, TEXT("Get Turn Servers Failed! [%d]: %s"), ErrorCode, *ErrorMessage);
}));

Get closest TURN server

This function automatically retrieves a list of all available TURN servers and pings all servers to provide the closest region.

FMultiRegistry::GetApiClient()->TurnManager.GetClosestTurnServerV2(THandler<FAccelByteModelsTurnServer>::CreateLambda(
[](const FAccelByteModelsTurnServer& Result)
{
UE_LOG(LogTemp, Log, TEXT("Get Closest Turn Server Success!"));
}),
FErrorHandler::CreateLambda([](const int32 ErrorCode, const FString& ErrorMessage)
{
UE_LOG(LogTemp, Log, TEXT("Get Closest Turn Server Failed! [%d]: %s"), ErrorCode, *ErrorMessage);
}));

Get TURN server latency by region

This function returns the total latency in milliseconds using a specific region in a parameter.

FMultiRegistry::GetApiClient()->TurnManager.GetTurnServerLatencyByRegion(Region, THandler<int32>::CreateLambda(
[&FuncName, &OutResponse, &bIsOk, &bIsDone](const int32& Result)
{
UE_LOG(LogTemp, Log, TEXT("Get Turn Server Latency By Region Success!"));
}),
FErrorHandler::CreateLambda([&bIsDone](const int32 ErrorCode, const FString& ErrorMessage)
{
UE_LOG(LogTemp, Log, TEXT("Get Turn Server Latency By Region Failed! [%d]: %s"), ErrorCode, *ErrorMessage);
}));

Get TURN server latencies

This function returns all available TURN server regions latencies.

FMultiRegistry::GetApiClient()->TurnManager.GetTurnServerLatencies(THandler<TArray<TPair<FString, float>>>::CreateLambda(
[&FuncName, &OutResponse, &bIsOk, &bIsDone](const TArray<TPair<FString, float>>& Result)
{
UE_LOG(LogTemp, Log, TEXT("Get Turn Server Latencies Success!"));
}),
FErrorHandler::CreateLambda([&bIsDone](const int32 ErrorCode, const FString& ErrorMessage)
{
UE_LOG(LogTemp, Log, TEXT("Get Turn Server Latencies Failed! [%d]: %s"), ErrorCode, *ErrorMessage);
}));

Get cached latencies

This function returns cached latencies. The AGS Unreal SDK caches latencies after pinging the available regions.

FMultiRegistry::GetApiClient()->TurnManager.GetCachedLatencies();