Skip to main content

Configure P2P using AGS OSS

Last updated on May 6, 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.

How it works

This section contains diagrams showing how P2P will work in AGS using the AGS online subsystem (OSS).

Host a game

Client join a game

Set up matchmaking V2 configuration

Follow these steps 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.

    Image shows 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 V2 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.

Setup

To initiate matchmaking in your game implementation, set up the following:

  • 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;
}

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

Here is a full example of a callback function you 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, you can use the Relay (TURN) as the Interactive Connectivity Establishment (ICE) connection by passing the additional parameter, -iceforcerelay, during the game launch.