Skip to main content

Manage P2P sessions with a session browser in AGS

Last updated on September 21, 2023

Overview

This guide will walk you through the basics of hosting, joining, and browsing peer-to-peer sessions using AccelByte's Online Subsystem (OSS) for Unreal Engine. This allows game clients to connect and play together without the need for a dedicated server. Additionally, a session browser implementation allows players to find and join existing open Peer-2-Peer (P2P) sessions without having knowledge of the host player.

Goals

  • Configure AccelByte Unreal Engine plugins
  • Host a peer-to-peer session
  • Browse and join P2P sessions

Prerequisites

Before you begin this guide, you should have the following:

  • Knowledge of Unreal Engine, including use of the Online Subsystem (OSS)
  • The AccelByte SDK, NetworkUtilities, and OnlineSubsystem plugins
  • Access to the AccelByte Admin Portal and the Namespace for your game
  • A session template with the type set to P2P and joinability set to OPEN

How to Configure the Plugins

To begin with, you'll need to enable V2 sessions in your DefaultEngine.ini file:

[OnlineSubsystemAccelByte]
bEnableV2Sessions=true

Then, you'll want to set up the turn server and net driver:

[AccelByteNetworkUtilities]
UseTurnManager=true
TurnServerSecret=<your-turn-secret>

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

Then, you'll need to configure net driver definitions per-platform. For instance, with Windows, you'd add the following to your WindowsEngine.ini:

[/Script/Engine.GameEngine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="AccelByteNetworkUtilities.IpNetDriverAccelByte",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
+NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver")

How to Host a P2P Session

Here is an example of creating a session using our new session template from the previous step. First, you'll need to do some setup, then you'll call CreateSession on the OSS session interface.

To begin with, we'll grab the AccelByte session interface:

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

FOnlineSessionV2AccelBytePtr SessionInterface;
if (!FOnlineSessionV2AccelByte::GetFromSubsystem(Subsystem, SessionInterface))
{
return;
}

Then, we'll configure our session settings:

FOnlineSessionSettings NewSessionSettings;

// This would be the name of the session template created in the Admin Portal
NewSessionSettings.Set(SETTING_SESSION_TEMPLATE_NAME, TEXT("P2PSession"));

// We want the new session to be a game session, as opposed to a party
// session
NewSessionSettings.Set(SETTING_SESSION_TYPE, SETTING_SESSION_TYPE_GAME_SESSION);

// We need some kind of parameter that the session browser can later use
// to query for sessions
NewSessionSettings.Set(FName(TEXT("IS_P2P_SESSION")), TEXT("true"));

// At this point, any other custom settings can be applied. For example, we
// can add a map name that we'll later use when we're hosting the P2P session
NewSessionSettings.Set(SETTING_MAPNAME, TEXT("MapName"));

And lastly, we make the call to actually create the session:

// We create a delegate which will be triggered when session creation is
// complete, inside of which we'll call StartSession and perform a travel
const FOnCreateSessionCompleteDelegate OnCreateSessionCompleteDelegate =
FOnCreateSessionCompleteDelegate::CreateUObject(this,
&MyClass::OnCreateSessionComplete);
FDelegateHandle CreateSessionDelegateHandle =
SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(
OnCreateSessionCompleteDelegate);
// Here `PlayerId` is an FUniqueNetIdPtr for the player
SessionInterface->CreateSession(
PlayerId.ToSharedRef().Get(), NAME_GameSession, NewSessionSettings);

In the above code, we add a handler for the session creation complete delegate. Inside that handler, we'll need to do the following:

First, we'll quickly check that this is the correct session using the SessionName delegate parameter:

if(SessionName != NAME_GameSession)
{
return;
}
note

It's a good idea to perform the above sanity check in any of the delegate handlers that receive a session name as a parameter.

Then we'd again grab the session interface, as above. After that, we can grab the actual session instance and mark the session as started:

FNamedOnlineSession* Session = SessionInterface->GetNamedSession(SessionName);
if (!ensure(Session != nullptr))
{
return;
}

SessionInterface->StartSession(SessionName);

Lastly, we'll construct a travel URL and perform the travel:

// We'll use our previously set map name for traveling
FString MapName;
Session->SessionSettings.Get(SETTING_MAPNAME, MapName);

// Constructing the travel URL with "?listen" appended to the map name so that we
// host a listen server
const FString TravelUrl = FString::Printf(TEXT("%s?listen"), *MapName);

// Here `PlayerController` is a pointer to an APlayerController for the
// local player
Controller->ClientTravel(TravelUrl, TRAVEL_Absolute);
note

Generally, it can be helpful to add in a session setting indicating whether the session is ready for other players to join, which would be set to some indicative value after the map loads.

How to Browse and Join P2P Sessions

In order to browse for P2P sessions, we're going to use the session interface's FindSessions method with the IS_P2P_SESSION setting from the previous step. To begin with, we'll need to again grab the AccelByte session interface. Then, we'll set up the attributes we want to query with:

TSharedPtr<FOnlineSessionSearch> QuerySessionsHandle =
MakeShared<FOnlineSessionSearch>();

// We'll set the maximum search results to some arbitrary value
QuerySessionsHandle->MaxSearchResults = 100;

// Search for sessions with the P2P setting we used earlier
QuerySessionsHandle->QuerySettings.Set(
FName(TEXT("IS_P2P_SESSION")), TEXT("true"));

// We can also query for other session settings, such as MAPNAME
QuerySessionsHandle->QuerySettings.Set(SETTING_MAPNAME, TEXT("MapName"));
note

You'll want to keep the FOnlineSessionSearch handle around for use in the FindSessions completion delegate, as it will contain the search results.

Next, we'll make the call to FindSessions:

const FOnFindSessionsCompleteDelegate OnFindSessionsCompleteDelegate = 
FOnFindSessionsCompleteDelegate::CreateUObject(
this, &MyClass::OnFindSessionsComplete);
FDelegateHandle FindSessionsCompleteDelegateHandle =
SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(
OnFindSessionsCompleteDelegate);
// Here `PlayerId` is an FUniqueNetIdPtr for the player
SessionInterface->FindSessions(
PlayerId.ToSharedRef().Get(), QuerySessionsHandle.ToSharedRef());

Inside our handler for the OnFindSessionsComplete delegate, we'll then have access to an array of FOnlineSessionSearchResult, which could be used to display a list of sessions in a session browser UI. For example, the handler often would pass this array to another delegate and then reset the search handle:

SomeSessionBrowserListingDelegate.Broadcast(QuerySessionsHandle->SearchResults);
QuerySessionsHandle.Reset();

In order to join one of these sessions, the client simply invokes the JoinSession method, passing one of the session search results from the array mentioned above. First, we'd grab the session interface again, and then we'd perform the join:

const FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate = 
FOnJoinSessionCompleteDelegate::CreateUObject(
this, &MyClass::OnJoinSessionComplete);
FDelegateHandle JoinSessionDelegateHandle =
SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(
OnJoinSessionCompleteDelegate);

// Here `PlayerId` is an FUniqueNetIdPtr for the player, and Session is
// simply an instance of FOnlineSessionSearchResult
return SessionInterface->JoinSession(
PlayerId.ToSharedRef().Get(), NAME_GameSession, Session);
note

It is generally good practice, before a join, to check if the player is already in a session using GetNamedSession. If so, we'd want to call DestroySession, and then join the session once the delegate for that method is triggered.

Inside the JoinSessionComplete delegate handler, we'd want to then perform a travel. Again, we'd start by grabbing the session interface, then we'd grab the travel URL and perform the client travel:

FString TravelUrl{};
// Here SessionName is a parameter from the join delegate
if (SessionInterface->GetResolvedConnectString(SessionName, TravelUrl,
NAME_GamePort) && !TravelUrl.IsEmpty())
{
// Here `PlayerController` is a pointer to an APlayerController for
// the local player
PlayerController->ClientTravel(TravelUrl, TRAVEL_Absolute);
}
note

For P2P sessions, the travel URL will be of the format accelbyte.<host_user_id>:<port>. In this case, the call to fetch the resolved connect string is simply generating the travel URL from the local session information.

Troubleshooting

In this section, you can find common errors and issues that may occur when using the service, along with recommendations on how to resolve them.

Players joining too early

It's possible to encounter an issue where a joining player (non-host) tries to travel to the server before the map has loaded.

Suggestions

A solution for this is to add a session setting (e.g.. SETTING_JOIN_READY) which would be set to something like “true” inside a delegate added to FCoreUObjectDelegates::PostLoadMapWithWorld:

FOnlineSessionSettings* SessionSettings = 
SessionInterface->GetSessionSettings(NAME_GameSession);
SessionSettings->Set(FName(TEXT("JOIN_READY")), TEXT("true"));

SessionInterface->UpdateSession(NAME_GameSession, *SessionSettings);

Then, on the joiner side, you'd want to listen to the UpdateReceived delegate on the session interface and look for that session setting before attempting the join. It could also be helpful to add this parameter to the query settings in your session browser's use of FindSessions so that the browser only displays sessions that are ready to be joined.