P2P Battles (beta)
Overview
PGOS provides the following functionalities to help game developers build p2p games:
- P2P Matchmaking
- P2P Battle Session
- P2P Connection
- P2P NetDriver
1. P2P Matchmaking
1.1 Set up a Matchmaking Configuration for P2P Battle Session
Firstly, you need to have a P2P Placer and set it as the Associated Placer
in any matchmaking config. Once configured, players matched through this matchmaking config will be allocated to a P2P battle session and undergo a placement process that differs from standard online battle sessions.
- Refer to this document for placer management.
- Refer to this document for details on the matchmaking configuration.
1.2 Workflow for P2P Matchmaking
- NAT Type Detection: Note that NAT type detection MUST be completed before initiating P2P matchmaking, otherwise the
StartP2PMatchmaking
interface will immediately return a failure. - Initiate P2P Matchmaking: The game client calls the
StartP2PMatchmaking
interface to trigger matchmaking. Ensure the match configuration complies with the settings defined in Section 2.1; otherwise, the resulting battle session will be placed in a fleet associated with the online placer instead of the host game client. Match results are notified to all participants via theOnMatchmakingProcessChanged
event, which includes the host player’s ID. - P2P Battle Session Placement: Upon successful matching, the P2P Match Server will request the P2P Battle Server to place the battle session. The session is ultimately assigned to the host client through the
OnStartP2PBattleSession
event. This workflow will be detailed in the next section.
2. P2P Battle Session
Unlike online battle sessions, PGOS manages P2P battle sessions through game clients instead of dedicated servers or local dedicated servers. Currently, PGOS users can only create P2P battle sessions via the P2P Matchmaking service for use by a host and connected clients. This chapter explains the session management capabilities and associated data of P2P battle sessions.
2.1 Workflow for P2P Battle Sessions
Place a P2P Battle Session on the Host Game Client
- P2P battle sessions are placed on the host game client via the PGOS P2P Battle Server. The host can monitor the
OnStartP2PBattleSession
event to receive placement requests. - Non-host participants should listen to the
OnP2PBattleSessionUpdated
event for real-time updates on session placement. - PGOS recommends all P2P battle session participants subscribe to these events, as all players hold peer roles until the host is confirmed. Implementation specifics may vary based on your game logic.
Activate a P2P Battle Session on the Host Game Client
- The host game client must call the
ActivateP2PBattleSession
interface at the appropriate time to transition the session state fromPlacing
toActive
. Participants will receive status updates via theOnStartP2PBattleSession
event. - PGOS recommends activating the session only after the host completes game preparations and is ready to accept P2P connections. Clients should establish P2P connections with the host after observing the session state change to
Active
.
Manage Players in P2P Battle Session
- PGOS assigns a player battle session ID to each participant. The host can validate a client's identity using this ID by calling the
AcceptP2PPlayerBattleSession
interface. - The host may terminate a player's session by invoking
RemoveP2PPlayerBattleSession
. Once removed, the associated player battle session ID becomes invalid for futureAcceptP2PPlayerBattleSession
calls.
Terminate a P2P Battle Session
- When ending a game, the host game client must call
TerminateP2PBattleSession
. PGOS will then destroy the session instance but retain transaction logs. This halts all PGOS-managed operations, including state synchronization and player session management. - Automatic termination occurs if the host goes offline (e.g., via
LogoutPGOS
, process crashes, or network disconnection). - Participants receive the
OnP2PPlayerBattleSessionsTerminated
event upon termination and may immediately seek new sessions.
2.2 Life-cycle Control
This section details the state machine of P2P (player) battle sessions. Understanding these state transitions is critical for precise control over session instances.
P2P Battle Session Lifecycle
- Placing: The P2P battle session has been assigned to the host but awaits activation.
- Active: The session is activated by the host. Game clients should attempt to connect to the host client upon observing this state.
- TimedOut: PGOS automatically destroys the session if the host fails to activate it within the allowed timeframe.
- Terminated: The session is ended either manually by the host or automatically by the PGOS P2P Battle Service when the host goes offline.
P2P player battle session Lifecycle
- Reserved: A slot is reserved for the player in the session.
- Active: Transitions to this state after the successful invocation of
AcceptP2PPlayerBattleSession
. - Completed: The terminal state of a player battle session. Calling
AcceptP2PPlayerBattleSession
will fail in this state. Triggers include:- Host-initiated: The host calls
RemoveP2PPlayerBattleSession
when confirming (or requiring) the player’s disconnection. - Client-initiated: The game client calls
TerminateP2PPlayerBattleSession
to voluntarily exit the session.
- Host-initiated: The host calls
2.3 Inspect P2P Battle Sessions
Similar to online battle sessions, PGOS records logs for P2P battle sessions. These logs are accessible via the Portal for monitoring and auditing purposes.
P2P battle session list
P2P battle session detail
Player's P2P battle sessions
3. P2P Connection
3.1 Usage Flow
3.2 Initialize API
// Get API instance
auto P2PConnectionAPI = IPgosSDKCpp::Get().GetClientP2PConnectionAPI();
3.3 Detect NAT Type
3.3.1 Purpose of NAT Type Detection
The primary purpose of NAT type detection is to provide parameters for P2P matchmaking, rather than being a necessary step for P2P connection establishment. Specifically:
- Matching Optimization: Understanding the NAT type helps the matchmaking backend select more suitable peer nodes, improving connection success rates.
- Network Diagnostics: Helps developers understand the user's network environment, providing references for subsequent network optimizations.
// NAT type detection must be performed before initiating P2P matching
P2PConnectionAPI->DetectNatType(FPgosClientOnDetectNatType::CreateLambda(
[](const FPgosResult& Result, const FClientDetectNatTypeResult* Data) {
if (Result.err_code == (int32)Pgos::PgosErrCode::kSuccess) {
// SDK automatically caches the result, no manual handling required
UE_LOG(LogTemp, Display, TEXT("NAT Type detected"));
// After successful detection, P2P matching can be initiated
StartP2PMatchmaking();
} else {
UE_LOG(LogTemp, Error, TEXT("Detect NAT type failed: %s"), *Result.msg);
// Handle detection failure
HandleNatDetectionFailure();
}
}
3.3.2 Mechanism of NAT Type Detection
Mandatory Detection:
- The P2P matchmaking would fail before NAT type detection.
- NAT type detection must be completed at least once before initiating P2P matchmaking.
Automatic Caching Mechanism:
- After the first call to
DetectNatType
, the SDK automatically caches the detection result. - The cached result remains valid until the next call to
DetectNatType
. - It is recommended to re-detect when the network environment changes (e.g., switching between WiFi/4G).
- After the first call to
Matchmaking Integration:
- The matchmaking module automatically retrieves the cached NAT type information from the cache.
- Developers do not need to manually set or pass NAT type information.
Error Handling:
- If detection fails, prompt the user and retry.
- It is recommended to provide network diagnostic suggestions upon detection failure.
3.4 Initiate P2P Connection
FClientP2PConnectParams Params;
Params.peer_player_id = "target_player_id";
// The SDK does not validate the token; it simply passes it to the peer. The game must validate it on the peer side.
// If used with P2P matching, it is recommended to pass the battle session ID here.
Params.token = "token";
Params.connection_type = EClientP2PConnectionType::Unspecified;
FString ConnectionId = P2PConnectionAPI->P2PConnect(Params, FPgosClientOnP2PConnect::CreateLambda(
[](const FPgosResult& Result, const FClientP2PConnectResult* Data) {
if (Result.err_code == (int32)Pgos::PgosErrCode::kSuccess) {
UE_LOG(LogTemp, Display, TEXT("P2P connection established"));
} else {
UE_LOG(LogTemp, Error, TEXT("P2P connection failed: %s"), *Result.msg);
}
}
));
Notes:
- Each call to
P2PConnect
sends a new connection request to the peer. - If the call fails, the returned
ConnectionId
will be empty. - Users can choose the P2P connection type:
- Unspecified: Prefers P2P connection, with a fallback to Relay server connection after a short delay if P2P connection fails.
- P2P: Connects via P2P.
- Relay: Connects via Relay server.
- After a successful connection, the
connection_type
field in the callback indicates the final connection type. - The
P2PConnect
callback is executed only after the peer accepts the request and the connection is established/fails, or the peer rejects the request. If the peer does not process the request for a long time, an error callback is executed.
3.5 Handle The P2P Connection Request
The client needs to listen to the OnP2PConnectRequest
event and call AcceptP2PConnect
or RejectP2PConnect
based on the specific situation.
P2PConnectionAPI->OnP2PConnectRequest().AddLambda(
[=](const FClientP2PConnectRequestEvt& Event) {
UE_LOG(LogTemp, Display, TEXT("Received P2P connection request from %s"), *Event.peer_player_id);
// Accept connection
P2PConnectionAPI->AcceptP2PConnect(Event.connection_id, FPgosClientOnAcceptP2PConnect::CreateLambda(
[](const FPgosResult& Result, const FClientP2PConnectResult* Data) {
if (Result.err_code == (int32)Pgos::PgosErrCode::kSuccess) {
UE_LOG(LogTemp, Display, TEXT("P2P connection accepted"));
} else {
UE_LOG(LogTemp, Error, TEXT("Accept P2P connection failed: %s"), *Result.msg);
}
}
));
// Reject connection
P2PConnectionAPI->RejectP2PConnect(Event.connection_id);
}
);
Notes:
- When calling
AcceptP2PConnect
, the callback is executed only after the connection succeeds/fails.
3.6 Send P2P Message
FClientSendP2PMessageParams Params;
Params.connection_id = "connection_id";
Params.message = FPgosUtil::ConvertStringToBytes("Hello, P2P!");
FPgosResult Result = P2PConnectionAPI->SendP2PMessage(Params);
if (Result.err_code == (int32)Pgos::PgosErrCode::kSuccess) {
UE_LOG(LogTemp, Display, TEXT("P2P message sent successfully"));
} else {
UE_LOG(LogTemp, Error, TEXT("Failed to send P2P message: %s"), *Result.msg);
}
Notes:
- Messages can only be sent after a successful connection.
3.7 Close P2P Connection
FPgosResult Result = P2PConnectionAPI->P2PClose("connection_id");
if (Result.err_code == (int32)Pgos::PgosErrCode::kSuccess) {
UE_LOG(LogTemp, Display, TEXT("P2P connection closed successfully"));
} else {
UE_LOG(LogTemp, Error, TEXT("Failed to close P2P connection: %s"), *Result.msg);
}
Notes:
- Calling
P2PClose
on unprocessed P2P requests has no effect. - Calling
P2PClose
afterAcceptP2PConnect
but before the connection is established will directly close the current connection.
3.8 Get P2P Connection Information
Connection information includes: connection status, RTT latency, packet loss rate, etc.
3.8.1 Get All P2P Connection Information for a Specific Player
TArray<FClientP2PConnectionInfo> Connections = P2PConnectionAPI->GetP2PConnectionInfoByPlayerId("target_player_id");
for (const FClientP2PConnectionInfo& Info : Connections) {
UE_LOG(LogTemp, Display, TEXT("Connection ID: %s, RTT: %d"), *Info.connection_id, Info.rtt);
}
3.8.2 Get P2P Connection Information for a Specific Connection ID
FClientP2PConnectionInfo ConnectionInfo;
if (P2PConnectionAPI->GetP2PConnectionInfoByConnectionId("connection_id", ConnectionInfo)) {
UE_LOG(LogTemp, Display, TEXT("Connection ID: %s, RTT: %d"), *ConnectionInfo.connection_id, ConnectionInfo.rtt);
} else {
UE_LOG(LogTemp, Warning, TEXT("Invalid connection ID"));
}
3.8.3 Get All P2P Connection Information
TArray<FClientP2PConnectionInfo> AllConnections = P2PConnectionAPI->GetAllP2PConnectionInfo();
for (const FClientP2PConnectionInfo& Info : AllConnections) {
UE_LOG(LogTemp, Display, TEXT("Connection ID: %s, RTT: %d"), *Info.connection_id, Info.rtt);
}
Notes:
- The returned connection information is a snapshot at the time of the call and may not reflect real-time status.
- RTT latency, packet loss rate, and other information are updated approximately every 5 seconds.
3.9 Event Listening
// Listening P2P messages
P2PConnectionAPI->OnP2PMessage().AddLambda(
[](const FClientP2PMessageEvt& Event) {
UE_LOG(LogTemp, Display, TEXT("Received P2P message: %s"), *FPgosUtil::ConvertBytesToString(Event.message));
}
);
// Listening P2P connection close events
P2PConnectionAPI->OnP2PConnectionClosed().AddLambda(
[](const FClientP2PConnectionClosedEvt& Event) {
UE_LOG(LogTemp, Display, TEXT("P2P connection closed: %s"), *Event.connection_id);
}
);
4. P2P NetDriver
The PGOS P2P NetDriver is a custom Unreal Engine networking solution designed for peer-to-peer (P2P) multiplayer games. It handles NAT traversal and direct peer connections, enabling players to host or join sessions without dedicated servers. This document details its public API and usage guidelines.
The underlying communication of the P2P NetDriver is implemented using the P2P Connection interface from the PgosSDK. Of course, you can also directly use the P2P Connection to implement your own P2P game.
❗ Note:
Except for the
FPgosP2PConnectionAPI::DetectNatType
interface, you usually do not need to use any other interfaces inP2P Connection
if you useP2P NetDriver
.
P2P NetDriver
is still in the experimental version.
4.1 Interface Introduction
4.1.1 Host Initializes a P2P Connection
/**
* Host initializes a P2P connection. Other regular clients can connect only after the Host has been initialized.
*
* @param LocalPlayer Specify the local player that the host should be associated with.
*/
void HostSetupP2PConnection(ULocalPlayer* LocalPlayer);
4.1.2 Client Request a P2P Connection to the Host
/**
* Client request a p2p connection to the host.
*
* @param LocalPlayer Specify the local player that the client should be associated with.
* @param PeerPlayerId Host's player ID.
* @param ResultDelegate The result delegate after the API execution ends, and it will be called in the GAME THREAD.
*/
void ClientP2PConnect(ULocalPlayer* LocalPlayer, const FString& PeerPlayerId, FPgosNetDriverOnP2PConnect ResultDelegate);
4.1.3 Close All P2P Connections to This Host
/**
* Close all p2p connections to this host.
*
* @param LocalPlayer Specify the local player that the client should be associated with.
*/
void HostCloseAllP2PConnection(ULocalPlayer* LocalPlayer);
4.2 Steps for Using in P2P Battle
4.2.1 Integrate P2P NetDriver into the Project
- Configure
PgosNetDriver
as NetDriver andPgosConnection
as NetConnection in DefaultEngine.ini.
[/Script/Engine.Engine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="/Script/PgosNetDriver.PgosNetDriver",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
[/Script/PgosNetDriver.PgosNetDriver]
NetConnectionClassName="/Script/PgosNetDriver.PgosConnection"
ConnectionTimeout=80.0
InitialConnectTimeout=120.0
NetServerMaxTickRate=30
MaxNetTickRate=120
KeepAliveTime=0.2
MaxClientRate=100000
MaxInternetClientRate=100000
RelevantTimeout=5.0
SpawnPrioritySeconds=1.0
ServerTravelPause=4.0
- Depends on the
PgosNetDriver
module in YourGame.Build.cs.
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay",
"OnlineSubsystem", "OnlineSubsystemUtils", "PgosSDKCpp", "PgosSDKBp", "PgosNetDriver"});
4.2.2 Start P2P Battle
- Before starting matchmaking, successfully call
FPgosP2PConnectionAPI::DetectNatType
once to check the player's network type.
IPgosSDKCpp::Get(GetOwningLocalPlayer()).GetClientP2PConnectionAPI()->DetectNatType(FPgosClientOnDetectNatType::CreateLambda(
[this](const FPgosResult& Result, const FClientDetectNatTypeResult* Data)
{
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("DetectNatType Success"))
}
else
{
UE_LOG(LogTemp, Log, TEXT("DetectNatType Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg)
}
}));
- Use
FPgosMatchmakingAPI::StartP2PMatchmaking
to start P2P matchmaking.
const auto Callback = [this](const FPgosResult& Ret, const FClientStartMatchmakingInfo* Info)
{
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("StartP2PMatchmaking Success"))
} else
{
UE_LOG(LogTemp, Log, TEXT("StartP2PMatchmaking Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg)
}
};
FClientStartMatchmakingParams Params;
Params.configuration_name = TEXT("battle-1v1-Matchmaking-p2p");
Params.player_info_list = MatchmakingPlayerInfos;
IPgosSDKCpp::Get(GetOwningLocalPlayer()).GetClientMatchmakingAPI()->StartP2PMatchmaking(Params, Callback);
- After a successful match, the host will receive the
FPgosMatchmakingAPI::OnStartP2PBattleSession
event. At this point, the host can useFPgosNetDriverModule::HostSetupP2PConnection
to initialize the P2P connection, waiting for other players to connect. Then, the host enters the battle map and activates the session throughFPgosP2PBattleAPI::ActivateP2PBattleSession
.
void UMatchmakingScreen::NativeConstruct()
{
const auto P2PBattleApi = IPgosSDKCpp::Get(GetOwningLocalPlayer()).GetClientP2PBattleAPI();
P2PBattleApi->OnStartP2PBattleSession().AddUObject(this, &UMatchmakingScreen::OnP2PHostStartBattleSession);
}
void UMatchmakingScreen::OnP2PHostStartBattleSession(const FClientStartP2PBattleSessionEvt& Event)
{
const auto GameInstance = Cast<UPgosShooterGameInstance>(GetGameInstance());
if (GameInstance)
{
GameInstance->HostSaveP2PBattleSession(Event);
}
FPgosNetDriverModule::Get().HostSetupP2PConnection(GetOwningLocalPlayer());
GetOwningLocalPlayer()->GetSubsystem<UGamePlayUtilities>()->P2PHostTravel(Event.battle_session.battle_session_id,
*Event.battle_session.cur_player_battle_session_info.player_battle_session_id);
}
void APGOSBattleGameModeP2P::OnStartBattleSession(const FString& BattleSessionId, FString& ErrorMessage)
{
// ...
if (auto API = GetPgosClientP2PBattleAPI())
{
FClientActivateP2PBattleSessionParams Params;
Params.battle_session_id = CurrentBattleSession.battle_session.battle_session_id;
FPgosOnApiResult ResultDelegate;
ResultDelegate.BindLambda([](const FPgosResult& Result)
{
ULogBlueprintFunctionLibrary::Log(
FString::Printf(TEXT("ActivateP2PBattleSession. code=(%d), msg=(%s)"), Result.err_code, *Result.msg));
});
API->ActivateP2PBattleSession(Params, ResultDelegate);
}
// ...
}
- Other players will receive the
FPgosMatchmakingAPI::OnP2PBattleSessionUpdated
event. Once the host activates the session and the session status isActive
, they can connect to the host throughFPgosNetDriverModule::ClientP2PConnect
. After connecting successfully, they enter the battle map.
void UMatchmakingScreen::NativeConstruct()
{
const auto P2PBattleApi = IPgosSDKCpp::Get(GetOwningLocalPlayer()).GetClientP2PBattleAPI();
P2PBattleApi->OnP2PBattleSessionUpdated().AddUObject(this, &UMatchmakingScreen::OnP2PClientBattleSessionUpdated);
}
void UMatchmakingScreen::OnP2PClientBattleSessionUpdated(const FClientP2PBattleSessionUpdatedEvt& Event)
{
if (Event.battle_session.status == EClientP2PBattleSessionStatus::Active)
{
FPgosNetDriverOnP2PConnect Delegate;
Delegate.BindLambda([this, Event](const FPgosResult& Result, const FString& ServerAddr)
{
UE_LOG(LogTemp, Log,
TEXT("OnP2PClientBattleSessionUpdated P2PConnect. errcode=(%d), msg=(%s), ServerAddr=(%s)"),
Result.err_code, *Result.msg, *ServerAddr)
GetOwningLocalPlayer()->GetSubsystem<UGamePlayUtilities>()->P2PClientTravel(ServerAddr,
Event.battle_session.cur_player_battle_session_info.player_battle_session_id);
});
FPgosNetDriverModule::Get().ClientP2PConnect(GetOwningLocalPlayer(), Event.battle_session.host_player_id, Delegate);
}
}
Conduct the battle.
After the P2P battle ends, the host disconnects all P2P connections with other players through
FPgosNetDriverModule::HostCloseAllP2PConnection
.
void APGOSBattleGameModeP2P::Logout(AController* Exiting)
{
Super::Logout(Exiting);
// ...
FPgosNetDriverModule::Get().HostCloseAllP2PConnection(LocalPlayer);
// ...
}