P2P 对战 (beta)
概述
PGOS 提供以下功能,帮助游戏开发者构建 P2P 游戏:
- P2P 匹配
- P2P 对战会话
- P2P 连接
- P2P 网络驱动程序
1. P2P 匹配
1.1 设置 P2P 战斗会话的匹配配置
首先,您需要拥有一个 P2P Placer,并在任何匹配配置中将其设置为“关联Placer”。配置完成后,通过此匹配配置匹配的玩家将被分配到 P2P 战斗会话,并经历与标准在线战斗会话不同的匹配流程。
1.2 P2P 匹配流程
- NAT 类型检测:请注意,NAT 类型检测必须在启动 P2P 匹配之前完成,否则
StartP2PMatchmaking
接口将立即返回失败。 - 启动 P2P 匹配:游戏客户端调用
StartP2PMatchmaking
接口触发匹配。请确保匹配配置符合 2.1 节中定义的设置;否则,生成的战斗会话将被放置到与在线匹配者关联的舰队中,而不是主机游戏客户端。匹配结果将通过OnMatchmakingProcessChanged
事件通知所有参与者,该事件包含主机玩家的 ID。 - P2P 战斗会话放置:匹配成功后,P2P 匹配服务器将请求 P2P 战斗服务器放置战斗会话。该会话最终通过
OnStartP2PBattleSession
事件分配给主机客户端。此工作流程将在下一节中详细介绍。
以下是专业的技术翻译:
在 P2P 对战中选择主机玩家时,我们遵循以下原则:
- 我们根据 NAT 类型对匹配的玩家进行排序
- 从开放类型开始,我们优先选择 NAT 类型更合适的玩家作为主机
- 无论 NAT 类型如何,主机都会被选中,即使是 SymmetricUDPFirewall 或 UDPBlocked
以下是 PGOS 对 NAT 类型的定义,您可以在 PGOS 客户端 SDK 中找到:
enum class NatType : uint8_t {
Unknown = 0,
/** No Nat */
Open = 1,
/** Full Cone */
FullCone = 2,
/** Address restricted cone */
AddressRestrictedCone = 3,
/** Port restricted cone */
PortRestrictedCone = 4,
/** Symmetric */
Symmetric = 5,
/** No NAT with UDP firewall */
SymmetricUDPFirewall = 6,
UDPBlocked = 7,
};
2. P2P 战斗会话
与在线战斗会话不同,PGOS 通过游戏客户端而非DS或 Local DS管理 P2P 战斗会话。目前,PGOS 用户只能通过 P2P 匹配服务创建 P2P 战斗会话,供主机和连接的客户端使用。本章介绍 P2P 战斗会话的会话管理功能及其相关数据。
2.1 P2P 战斗会话流程
在主机游戏客户端上放置 P2P 对战会话
P2P 对战会话通过 PGOS P2P 对战服务器 在主机游戏客户端上放置。主机可以监听
OnStartP2PBattleSession
事件以接收放置请求。非主机参与者应监听
OnP2PBattleSessionUpdated
事件以获取会话放置的实时更新。PGOS 建议所有 P2P 对战会话参与者订阅这些事件,因为在主机确认之前,所有玩家都将保持对等角色。具体实现细节可能因游戏逻辑而异
在主机游戏客户端上激活 P2P 对战会话
- 主机游戏客户端必须在适当的时间调用
ActivateP2PBattleSession
接口,将会话状态从Placing
转换为Active
。参与者将通过OnStartP2PBattleSession
事件接收状态更新。 - PGOS 建议仅在主机完成游戏准备并准备好接受 P2P 连接后才激活会话。客户端应在观察到会话状态变为
Active
后与主机建立 P2P 连接。
管理 P2P 对战会话中的玩家
- PGOS 会为每个参与者分配一个玩家对战会话 ID。主机可以通过调用
AcceptP2PPlayerBattleSession
接口,使用此 ID 验证客户端的身份。 - 主机可以通过调用
RemoveP2PPlayerBattleSession
来终止玩家的会话。一旦移除,关联的玩家对战会话 ID 将失效,无法用于后续的AcceptP2PPlayerBattleSession
调用。
终止 P2P 对战会话
- 游戏结束时,主游戏客户端必须调用
TerminateP2PBattleSession
。PGOS 将销毁会话实例,但保留事务日志。这将停止所有 PGOS 管理的操作,包括状态同步和玩家会话管理。 - 如果主游戏玩家离线(例如,通过
LogoutPGOS
、进程崩溃或网络断开连接),则会自动终止。 - 参与者在终止时会收到
OnP2PPlayerBattleSessionsTerminated
事件,并可以立即寻求新的会话。
2.2 生命周期控制
本节详细介绍了 P2P(玩家)战斗会话的状态机。了解这些状态转换对于精确控制会话实例至关重要。
P2P 战斗会话生命周期
- Placing:P2P 对战会话已分配给主机,但等待激活。
- Active: 会话已由主机激活。游戏客户端在检测到此状态后应尝试连接到主机客户端。
- TimedOut:如果主机未能在允许的时间内激活会话,PGOS 将自动销毁会话。
- Terminated:会话将由主机手动终止,或由主机离线时PGOS P2P 对战服务自动终止。
P2P 玩家战斗会话生命周期
- Reserved:会话中为玩家保留一个位置。
- Active:成功调用
AcceptP2PPlayerBattleSession
后转换到此状态。 - 已完成:玩家战斗会话的终止状态。在此状态下调用
AcceptP2PPlayerBattleSession
将失败。触发器包括: - Host-initiated:主机在确认(或要求)玩家断开连接时调用
RemoveP2PPlayerBattleSession
。 - Client-initiated:游戏客户端调用
TerminateP2PPlayerBattleSession
主动退出会话。
2.3 主机迁移
与在线对战相比,P2P 对战对网络环境的要求更高,这意味着游戏中的主机可能会遇到问题。这些问题可能包括由于主机断线导致的对战终止,或客户端与主机之间不稳定的延迟,从而对游戏体验造成负面影响。
为了解决这个问题,PGOS 为 P2P 对战对战提供了主机迁移支持。您可以在 P2P Placer中开启“启用主机迁移”选项来启用此功能。
PGOS 提供主机迁移工作流支持,当原主机出现异常状态时,会自动从玩家中选择新的主机。如果您希望在主机迁移后从之前的进度继续游戏,则需要在游戏逻辑中实现游戏状态备份和同步功能。
PGOS 不限制主机迁移的次数。每次选择新主机的算法与初始主机选择相同。有关算法详情,请参阅上一篇文章。
我们添加了两个新状态来支持 P2P 战斗会话的主机迁移:
- HostHealthChecking - 进入此状态后,会话中所有非主机玩家必须在指定时间内通过“ReportHostHealth”接口报告主机健康状况。PGOS 将根据 P2P Placer 中配置的阈值,决定是保留当前主机还是选择新的主机。P2P 战斗会话通过以下任一触发条件进入“HostHealthChecking”状态:
- 任何对等客户端为活跃的 P2P 战斗会话调用“ReportHostHealth”
- PGOS 后端检测到主机客户端在线玩家会话中的异常
- NewHostSelected - 此状态表示成功选出新主机。新主机必须在规定时间内调用“ActivateP2PBattleSession”才能恢复游戏,否则会话将终止。新主机也可以调用“RejectHostMigrating”来中止迁移,这也会终止会话。
所有 P2P 战斗会话状态转换均通过“OnP2PBattleSessionUpdated”事件推送。请密切关注此事件以获取实时会话状态更新。****
2.4 检查 P2P 战斗会话
与在线战斗会话类似,PGOS 会记录 P2P 战斗会话的日志。您可以通过 门户 访问这些日志,以进行监控和审计。
P2P 战斗会话列表**
P2P 战斗绘画详情
玩家 P2P 战斗会话详情
3. P2P 连接
3.1 使用流程
3.2 初始化 API
// Get API instance
auto P2PConnectionAPI = IPgosSDKCpp::Get().GetClientP2PConnectionAPI();
3.3 检测 NAT 类型
3.3.1 NAT 类型检测的目的
NAT 类型检测的主要目的是为 P2P 匹配提供参数,而非 P2P 连接建立的必要步骤。具体而言:
- 匹配优化:了解 NAT 类型有助于匹配后端选择更合适的对等节点,从而提高连接成功率。
- 网络诊断:帮助开发者了解用户的网络环境,为后续网络优化提供参考。
// 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 NAT 类型检测机制
- 强制检测:
- 在 NAT 类型检测之前,P2P 匹配会失败。
- 发起 P2P 匹配之前,必须至少完成一次 NAT 类型检测。
- 自动缓存机制:
- 首次调用
DetectNatType
后,SDK 会自动缓存检测结果。 - 缓存结果有效,直到下次调用
DetectNatType
。 - 建议在网络环境发生变化时(例如在 WiFi/4G 之间切换)重新检测。
- 首次调用
- 匹配集成:
- 匹配模块会自动从缓存中检索缓存的 NAT 类型信息。
- 开发者无需手动设置或传递 NAT 类型信息。
- 错误处理:
- 如果检测失败,提示用户并重试。
- 建议在检测失败时提供网络诊断建议。
3.4 初始化 P2P 连接
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);
}
}
));
- 注意事项
- 每次调用
P2PConnect
都会向对端发送一个新的连接请求。 - 如果调用失败,返回的
ConnectionId
将为空。 - 用户可以选择 P2P 连接类型:
- 未指定:优先使用 P2P 连接,如果 P2P 连接失败,则在短暂延迟后回退到中继服务器连接。
- P2P:通过 P2P 连接。
- Relay:通过中继服务器连接。
- 连接成功后,回调中的
connection_type
字段指示最终的连接类型。 - 仅在对端接受请求且连接建立/失败,或对端拒绝请求后,才会执行
P2PConnect
回调。如果对端长时间未处理请求,则会执行错误回调。
- 每次调用
3.5 处理 P2P 连接请求
客户端需要监听 OnP2PConnectRequest
事件,并根据具体情况调用 AcceptP2PConnect
或 RejectP2PConnect
。
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);
}
);
注意事项
调用 AcceptP2PConnect
时,仅在连接成功/失败后执行回调。
3.6 发行 P2P 消息
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);
}
注意事项
连接成功后才能发送消息。
3.7 关闭 P2P 连接
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);
}
注意:
- 对未处理的 P2P 请求调用
P2PClose
无效。 - 在
AcceptP2PConnect
之后、连接建立之前调用P2PClose
将直接关闭当前连接。
3.8 获取 P2P 连接信息
连接信息包括:连接状态、RTT 延迟、丢包率等。
3.8.1 获取特定玩家的所有 P2P 连接信息
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 获取特定连接ID的P2P连接信息
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 获取所有P2P连接信息
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);
}
注意:
- 返回的连接信息是调用时的一个快照,可能无法反映实时状态。
- RTT 延迟、丢包率和其他信息大约每 5 秒更新一次。
3.9 事件监听
// 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 网络驱动程序
PGOS P2P 网络驱动程序是一款专为P2P多人游戏设计的虚幻引擎自定义网络解决方案。它能够处理 NAT 遍历和直接对等连接,使玩家无需专用服务器即可发起或加入会话。本文档详细介绍了其公共 API 和使用指南。
P2P 网络驱动程序的底层通信是使用 PgosSDK 中的 P2P 连接 接口实现的。当然,您也可以直接使用 P2P 连接 来实现您自己的 P2P 游戏。
❗ 注意:
除了
FPgosP2PConnectionAPI::DetectNatType
接口外,如果您使用P2P NetDriver
,通常不需要使用P2P Connection
中的任何其他接口。
P2P NetDriver
仍处于实验版本。
4.1 接口介绍
4.1.1 主机初始化 P2P 连接
/**
* 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 客户端请求与主机建立P2P连接
/**
* 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 关闭到此主机的所有P2P连接
/**
* 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 P2P 对战使用步骤
4.2.1 将 P2P NetDriver 集成到项目中
- 在 DefaultEngine.ini 文件中,将
PgosNetDriver
配置为 NetDriver,将PgosConnection
配置为 NetConnection。
[/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
- 依赖于 YourGame.Build.cs 中的“PgosNetDriver”模块。
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay",
"OnlineSubsystem", "OnlineSubsystemUtils", "PgosSDKCpp", "PgosSDKBp", "PgosNetDriver"});
4.2.2 开始 P2P 对战
- 在开始匹配之前,成功调用一次
FPgosP2PConnectionAPI::DetectNatType
来检查玩家的网络类型。
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)
}
}));
- 使用“FPgosMatchmakingAPI::StartP2PMatchmaking”开始 P2P 匹配。
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);
- 匹配成功后,主机会收到
FPgosMatchmakingAPI::OnStartP2PBattleSession
事件。此时,主机可以使用FPgosNetDriverModule::HostSetupP2PConnection
初始化 P2P 连接,等待其他玩家连接。之后,主机进入对战地图,并通过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);
}
// ...
}
- 其他玩家会收到
FPgosMatchmakingAPI::OnP2PBattleSessionUpdated
事件。当主机激活会话,且会话状态为Active
时,其他玩家可以通过FPgosNetDriverModule::ClientP2PConnect
连接主机。连接成功后,即可进入对战地图。
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);
}
}
进行战斗。
P2P 战斗结束后,主机将通过
FPgosNetDriverModule::HostCloseAllP2PConnection
断开与其他玩家的所有 P2P 连接。
void APGOSBattleGameModeP2P::Logout(AController* Exiting)
{
Super::Logout(Exiting);
// ...
FPgosNetDriverModule::Get().HostCloseAllP2PConnection(LocalPlayer);
// ...
}