跳到主要内容

P2P Battles (beta)

Overview

PGOS provides the following functionalities to help game developers build p2p games:

  1. P2P Matchmaking
  2. P2P Battle Session
  3. P2P Connection
  4. 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.

image-20250317153505924

1.2 Workflow for P2P Matchmaking

sequenceDiagram participant A as PlayerA participant P as P2P Server participant M as P2P Match Server participant S as P2P Battle Server participant B as PlayerB rect rgb(170, 190, 253) note over A,B: Nat type detecting must be done before any P2P matchmaking request !! A->>P:call DetectNatType B->>P:call DetectNatType end rect rgb(170, 190, 253) note over A,B: Match server searching for players A->>M:StartP2PMatchmaking M->>M:Searching for players B->>M:StartP2PMatchmaking M->>M:Two players found, choose playerA as HOST M-->>A:OnMatchmakingProcessChanged(HOST=PlayerA, BattleSessionId=xxx) M-->>B:OnMatchmakingProcessChanged(HOST=PlayerA, BattleSessionId=xxx) end rect rgb(170, 190, 253) note over A,B: P2P battle session placement M->>S:Place a P2P battle session(HOST=PlayerA) S-->>A:Trigger OnStartP2PBattleSession enent end
  • 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 the OnMatchmakingProcessChanged 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

sequenceDiagram participant H as Host Game Client participant C as Game Client participant M as P2P Match Server participant S as P2P Battle Server rect rgb(170, 190, 253) note over H,S: Place a P2P battle session on Host game client M->>S: Place P2P battle session S-->>H: OnStartP2PBattleSession S-->>C: OnP2PBattleSessionUpdated(status=Placing) end H->>H: Prepare for the game rect rgb(170, 190, 253) note over H,S: Host game client active a P2P battle session H->>S: ActivateP2PBattleSession S-->>H: OnP2PBattleSessionUpdated(status=Active) S-->>C: OnP2PBattleSessionUpdated(status=Active) end rect rgb(170, 190, 253) note over H,S: Check for P2P client connection request C->>H: P2P connect to Host game client H->>+S: AcceptP2PPlayerBattleSession S-->>-H: Rsp: accept success or not end H->>H: P2P game ongoing rect rgb(170, 190, 253) note over H,S: Handle player exit C->>H: P2P disconnect from Host game client H->>S: RemoveP2PPlayerBattleSession end rect rgb(170, 190, 253) note over H,S: Host game client ends a P2P battle session H->>S: TerminateP2PBattleSession end

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 from Placing to Active. Participants will receive status updates via the OnStartP2PBattleSession 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 future AcceptP2PPlayerBattleSession 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

stateDiagram [*] --> Placing: Placement from P2P match server Placing --> Active: ActivateP2PBattleSession called by host Placing --> TimedOut: ActivateP2PBattleSession is not called Active --> Terminated: TerminateP2PBattleSession is called by host Active --> Terminated: Host client disconnected from PGOS Placing --> Terminated: Host client disconnected from PGOS Terminated --> [*] TimedOut --> [*]
  • 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

stateDiagram [*] --> Reserved: Player being added to a P2P battle session Reserved --> Active: AcceptP2PPlayerBattleSession is called by host Reserved --> Completed: RemoveP2PPlayerBattleSession is called by host Reserved --> Completed: TerminateP2PPlayerBattleSession is called by client Active --> Completed: RemoveP2PPlayerBattleSession is called by host Active --> Completed:TerminateP2PPlayerBattleSession is called by client Completed --> [*]
  • 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.

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

image-20250317161025945

P2P battle session detail

image-20250317161209176

Player's P2P battle sessions

image-20250317161333963

3. P2P Connection

3.1 Usage Flow

graph TD A[Initialize API] --> B{Need NAT Info?} B -->|Yes| C[Detect NAT Type] B -->|No| D[Initiate P2P Connection] C --> D D --> E{Received Connection Request?} E -->|Yes| F[Accept/Reject Connection] E -->|No| G[Wait] F --> H[Send P2P Message] H --> I[Close Connection]

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:

  1. Matching Optimization: Understanding the NAT type helps the matchmaking backend select more suitable peer nodes, improving connection success rates.
  2. 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

  1. Mandatory Detection:

    • The P2P matchmaking would fail before NAT type detection.
    • NAT type detection must be completed at least once before initiating P2P matchmaking.
  2. 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).
  3. 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.
  4. 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:

  1. Each call to P2PConnect sends a new connection request to the peer.
  2. If the call fails, the returned ConnectionId will be empty.
  3. Users can choose the P2P connection type:
    1. Unspecified: Prefers P2P connection, with a fallback to Relay server connection after a short delay if P2P connection fails.
    2. P2P: Connects via P2P.
    3. Relay: Connects via Relay server.
  4. After a successful connection, the connection_type field in the callback indicates the final connection type.
  5. 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:

  1. 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:

  1. 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:

  1. Calling P2PClose on unprocessed P2P requests has no effect.
  2. Calling P2PClose after AcceptP2PConnect 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:

  1. The returned connection information is a snapshot at the time of the call and may not reflect real-time status.
  2. 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 in P2P Connection if you use P2P 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 and PgosConnection 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 use FPgosNetDriverModule::HostSetupP2PConnection to initialize the P2P connection, waiting for other players to connect. Then, the host enters the battle map and activates the session through FPgosP2PBattleAPI::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 is Active, they can connect to the host through FPgosNetDriverModule::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);

// ...

}