跳到主要内容

组队

1. 概述

组队 是为玩家创建即时队伍的服务,玩家可以通过它组建 “临时虚拟队伍”,然后发起战斗。包括组队创建、玩家邀请、自定义数据、 组队 事件调度等功能。 组队中有一名队长和多名队员,所有玩家都有创建组队的权限。创建队伍后,创建者默认为队长,但必要时可以将队长身份转给其他队员。由于组队是即时队伍,玩家手动或离线退出组队后,将被移出组队 。

2. 架构图

游戏通过 PGOS 客户端 SDK 访问组队服务,包含两种交互,一种是通过 HTTPS 协议进行客户端调用,另一种是客户端事件 ,是 PGOS 后端的推送服务推送的消息,通过 PGOS 客户端 SDK 鉴权后自动建立并维护的 WebSocket 长连接实现,所有事件都由 SDK 以推送消息的形式触发,并通知游戏。

party_arch

3. 核心功能和规则

3.1 创建组队

任何玩家都可以创建组队,但一名玩家同时只能创建/加入一个组队。 从Party模块调用CreateParty,如下所示:

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeUObjectClass::CreateParty()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
FClientCreatePartyParams Params;
Params.party_name = "A Name";
Params.max_player = 0;
Params.enable_chatting = false;
Params.join_strategy = EClientPartyJoinStrategy::Public;
Params.invite_strategy = EClientPartyInviteStrategy::AllMembersCanInvite;
party->CreateParty(Params, [](const FPgosResult& Ret, const FClientPartyInfo* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("OnCreatePartySuccess: party_id=%s"), *Data->party_id);
}
else
{
UE_LOG(LogTemp, Log, TEXT("OnCreatePartyFailed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

3.2 组队邀请和加入控制

在上述CreateParty代码中,有两个参数join_strategyinvite_strategy,用于控制组队如何处理"加入"和"邀请"。 join_strategy控制其他玩家如何加入组队,有四种不同的策略:

EClientPartyJoinStrategyDescription
Public除了被队长屏蔽的玩家外,任何玩家都可以加入组队。
这是默认设置。
OnlyFriendsOfLeader仅允许队长的好友加入组队。
OnlyFriendsOfMembers只有未被队长屏蔽的任意队员的好友才能加入组队。
OnlyInvited只有被队长明确邀请且未被其屏蔽的玩家才能加入组队。

invite_strategy 控制组队成员如何邀请其他人。

EClientPartyInviteStrategyDescription
AllMembersCanInvite组队中的所有成员都可以邀请其他玩家加入组队。
被队长或邀请者屏蔽的玩家无法被邀请。
这是默认设置。
OnlyLeaderCanInvite只有队长可以邀请其他玩家加入组队。
被队长屏蔽的玩家无法被邀请。

组队创建后,只有队长可以更改加入策略邀请策略。 请按以下方式调用Party模块中的SetPartyStrategy

#include "PgosSDKCpp.h"

void SomeUObjectClass::SetPartyStrategy()
{
auto Party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (Party)
{
FClientSetPartyStrategyParams Params;
Params.party_id = PartyId;
Params.join_strategy = EClientPartyJoinStrategy::Public;
Params.invite_strategy = EClientPartyInviteStrategy::AllMembersCanInvite;
Party->SetPartyStrategy(Params, [](const FPgosResult& Ret) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("SetPartyStrategy Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("SetPartyStrategy Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

所有成员(除了队长)将收到PartyStrategy变更的通知。请按以下方式绑定动态委托到OnPartyStrategyChanged

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

// Observe the notification of party strategies changes somewhere
void SomeUObjectClass::SomeFunction()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
party->OnPartyStrategyChanged().AddUObject(this, &SomeUObjectClass::OnPartyStrategyChanged);
}
}

// Handle the notification
void SomeUObjectClass::OnPartyStrategyChanged(const FClientPartyStrategyChangedEvt& event)
{
UE_LOG(LogTemp, Log, TEXT("OnPartyStrategyChanged"));
// Party members cannot change `join_strategy` and `invite_strategy` actively.
}

3.3 邀请和加入

队长和队员可以邀请其他玩家加入组队,每位玩家同一时间只能加入一个队伍。

sequenceDiagram PlayerA->>PartyService: invite playerB PartyService-->>+PlayerB: notify inviting PlayerB->>-PartyService: PlayerB join PartyService-->>PlayerA: notify PlayerB joining PartyService-->>PlayerC: notify PlayerB joining

3.3.1 邀请

组队模块调用InvitePlayerToParty,具体如下:

#include "PgosSDKCpp.h"

void SomeUObjectClass::InvitePlayerToParty()
{
auto Party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (Party)
{

FString PartyId;
TArray<FString> InviteePlayerIds;
Party->InvitePlayerToParty(PartyId, InviteePlayerIds, [](const FPgosResult& Ret) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("InvitePlayerToParty Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("InvitePlayerToParty Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

按照以下方式将动态委托绑定到 OnReceivePartyInvitation

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

// Observe the notification of party invitation somewhere
void SomeUObjectClass::SomeFunction()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
party->OnReceivePartyInvitation().AddUObject(this, &SomeUObjectClass::OnReceivePartyInvitation);
}
}

// Handle the invitation
void SomeUObjectClass::OnReceivePartyInvitation(const FClientReceivePartyInvitationEvt& event)
{
UE_LOG(LogTemp, Log, TEXT("OnReceivePartyInvitation"));
// After receiving the invitation, it will generally be handled according to one of the following situations:
// 1. After receiving the invitation, the game will pop up a dialog box asking the player whether to join the party. After the player agrees, call the JoinParty API to join the party.
// 2. After receiving the invitation, the game can directly call the JoinParty API to join the party.
}

3.3.2 加入组队

按如下方式从 Party 模块调用 JoinParty

#include "PgosSDKCpp.h"

void SomeUObjectClass::JoinParty()
{
auto Party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (Party)
{
FString PartyId;
Party->JoinParty(PartyId, [](const FPgosResult& Ret, const FClientPartyInfo* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("JoinParty Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("JoinParty Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

3.4 队长特权

  • 踢出成员:队长可以随时踢出任何成员。
  • 解散组队。
  • 将队长转让给其他成员。 从 Party 模块调用 KickPartyMember/DismissParty/TransferPartyLeader 方法,如下所示:
#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

// Kick out members
void SomeUObjectClass::KickPartyMember()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
FString PartyId = ObtainFromSomeWhere();
FString KickPlayerID = ObtainFromSomeWhere();

party->KickPartyMember(PartyId, KickPlayerID, [](const FPgosResult& Ret) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("OnKickMemberSuccess"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("OnKickMemberFailed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

// Dismiss the party
void SomeUObjectClass::DismissParty()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
FString PartyId = ObtainFromSomeWhere();

party->DismissParty(PartyId, [](const FPgosResult& Ret) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("OnDismissPartySuccess"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("OnDismissPartyFailed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

// Transfer leader privilege to another member
void SomeUObjectClass::TransferPartyLeader()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
FString PartyId = ObtainFromSomeWhere();
FString NewLeaderPlayerId = ObtainFromSomeWhere();

party->TransferPartyLeader(PartyId, NewLeaderPlayerId, [](const FPgosResult& Ret) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("OnTransferLeaderSuccess"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("OnTransferLeaderFailed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

3.5 解散组队

在以下情况下组队将被解散:

  • 队长解散组队。

  • 当所有成员都离开组队或登出游戏后,组队将自动解散。

    sequenceDiagram PlayerA->>PartyService: dismiss PartyService-->>PlayerB: notify dismissed PartyService-->>PlayerC: notify dismissed

请参考上一节的示例代码。

3.6 退出组队

  • 任何玩家都可以退出组队。当队长退出组队时,PGOS 会自动将另一名成员提升为新的队长。
  • 玩家登出后将自动退出组队。
sequenceDiagram PlayerA->>PartyService: leave party / transfer leader PartyService-->>PlayerB: notify PlayerA left / leader transfered PartyService-->>PlayerC: notify PlayerA left / leader transfered

按照以下方式调用 Party 模块中的 LeaveParty

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

// Leave the party
void SomeUObjectClass::LeaveParty()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
FString PartyId = ObtainFromSomeWhere();

party->LeaveParty(PartyId, [](const FPgosResult& Ret) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("OnLeavePartySuccess"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("OnLeavePartyFailed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

3.7 自定义数据处理

组队自定义数据用于帮助游戏扩展组队功能,例如"组队准备就绪"、"组队投票"等。每个组队都有两层自定义数据:全局自定义数据玩家自定义数据。游戏开发者可以在其中存储任何数据来实现特定功能。

全局自定义数据是一个字符串,用于存储全局范围的数据。所有成员都可以访问,但在游戏客户端中只有组队队长有权限存储和修改。

玩家自定义数据是一个字符串到字符串(玩家ID -> 数据)的映射,用于存储每个组队成员的数据,其中键是玩家ID,值是由该成员设置的数据。

所有成员都可以访问,但玩家只能存储和修改自己的玩家自定义数据。 当任何自定义数据发生变化时(包括全局自定义数据和玩家自定义数据),

所有成员都会在游戏客户端收到PartyCustomDataChangedEvt事件。

sequenceDiagram PlayerA(leader)->>PartyService: modify global custom data PlayerB->>PartyService: modify playerB's custom data PartyService-->>PlayerB: notify custom data changed PartyService-->>PlayerA(leader): notify custom data changed PartyService-->>PlayerC: notify custom data changed

调用 GetPartyInfoCurrentParty 来获取包含 PartyCustomData 的 PartyInfo。 调用 SetGlobalCustomData 来设置组队的全局自定义数据。请注意,只有队长有权限进行此操作:

  #include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeUObjectClass::SetGlobalCustomData()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
FString PartyId = ObtainFromSomeWhere();

party->SetGlobalCustomData(PartyId, GlobalCustomData, [](
const FPgosResult& Ret, const PartyCustomData * Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("OnSetGlobalCustomDataSuccess"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("OnSetGlobalCustomDataFailed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

调用 SetPlayerCustomData 来设置玩家自定义数据。请注意,玩家只有权限存储和修改自己的玩家自定义数据:

  #include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeUObjectClass::SetPlayerCustomData()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
FString PartyId = ObtainFromSomeWhere();

party->SetPlayerCustomData(PartyId, PlayerCustomData, [](
const FPgosResult& Ret, const PartyCustomData * Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("OnSetPlayerCustomDataSuccess"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("OnSetPlayerCustomDataFailed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

3.8 开始匹配

由于设计上的考虑,队伍和匹配是完全解耦的,我们可以使用多人匹配请求,将所有玩家ID作为参数传入匹配请求接口。([参考匹配详细指南](./ 匹配 .md)) Leader可以通过以下流程开始匹配 :

  • Leader通过游戏UI启动匹配
  • 领队客户端获取各个成员的player_id,并生成player_id列表
  • 领队客户端将player_id列表作为核心参数传入,调用匹配服务中的StartMatchmaking接口
  • 成员客户端会立刻收到领队已启动匹配推送消息
  • 成员客户端调用JoinMatchmaking接口
  • 匹配成功后,所有玩家都会收到推送消息,提示可以加入战斗并连接专属服务器

3.9 加入房间

由于设计上的考虑,队伍和房间是完全解耦的,我们可以使用多人加入房间请求将所有玩家ID传入房间接口。

3.10 组队聊天

组队服务内置聊天功能,可直接使用:调用接口 UPgosClientAPICreateParty::CreatePartyEnableChatting 为 true。

  • 组队中任何成员均可发送组队聊天消息
  • 组队中任何成员均可接收组队聊天消息
  • 成员不会收到自己发送的组队聊天消息
sequenceDiagram participant A as PlayerA participant B as PlayerB participant C as PlayerC participant Chat as Party Service A->>Chat:SendPartyChatTextMsg(Message) Chat-->>A:return success(ClientChatMsgInfo) or failed Chat-->>B:OnReceivePartyChatMsg:ChatMsgInfo Chat-->>C:OnReceivePartyChatMsg:ChatMsgInfo

发送组队聊天消息组队模块调用SendPartyChatTextMsg/SendPartyChatCustomMsg,具体如下:

#include "PgosSDKCpp.h"

void SomeUObjectClass::SendPartyChatTextMsg()
{
auto Party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (Party)
{
FClientSendPartyChatMsgParams Params;
Party->SendPartyChatTextMsg(Params, [](const FPgosResult& Ret, const FClientChatMsgInfo* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("SendPartyChatTextMsg Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("SendPartyChatTextMsg Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

void SomeUObjectClass::SendPartyChatCustomMsg()
{
auto Party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (Party)
{
FClientSendPartyChatMsgParams Params;
Party->SendPartyChatCustomMsg(Params, [](const FPgosResult& Ret, const FClientChatMsgInfo* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("SendPartyChatCustomMsg Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("SendPartyChatCustomMsg Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

接收组队聊天消息 将动态委托绑定到OnReceivePartyChatMsg,具体如下:

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeUObjectClass::SomeFunction()
{
auto party = IPgosSDKCpp::Get().GetClientPartyAPI();
if (party)
{
party->OnReceivePartyChatMsg().AddUObject(this, &SomeUObjectClass::OnReceiveChatMsg);
}
}

void SomeUObjectClass::OnReceiveChatMsg(const FClientReceivePartyChatMsgEvt& event)
{
UE_LOG(LogTemp, Log, TEXT("OnReceiveChatMsg"));
}

3.11 在门户网站中查询组队

您可以在网页门户中查询组队记录。当创建组队时,PGOS后端会自动生成并存储一条历史记录,其中包含以下信息:

  • 队长玩家ID

  • 成员玩家ID:成员可能在组队存在期间发生变化,成员信息将记录在此

  • 组队的创建时间和解散时间(如果已解散)

  • 组队ID、名称

  • 创建者设定的最大允许玩家数量

查询界面如下所示:组队列表

1605498590504

组队详情

1605498539738

3.12 组队事件

Event Name事件描述
OnPartyStrategyChanged当组队策略改变时发送。
OnPartyMemberJoined当新玩家加入组队时发送。
OnPartyMemberLeft当玩家离开组队时发送。
OnPartyDismissed当组队解散时发送。
OnPartyLeaderTransferred当领导者状态被转移时发送。
OnReceivePartyInvitation当收到组队邀请时发送。
OnReceivePartyChatMsg当收到组队聊天消息时发送。
OnCustomDataChanged当组队自定义数据发生变更时发送。
OnPartyInfoUpdated当组队信息更新时发送。
注意

本节讨论一个特殊的事件 OnPartyInfoUpdated,当组队信息发生变化时会触发该事件。 通常,当组队信息发生变化时,会触发更具体的事件。例如,当组队策略改变时会触发 OnPartyStrategyChanged 事件,当新玩家加入组队时会触发 OnPartyMemberJoined 事件。 然而,由于网络问题或其他原因导致持久连接中断,服务器可能无法通过此连接通知客户端组队信息的变化(如玩家进入或离开组队)。如果客户端长时间无法感知组队信息的变化,可能会导致游戏功能异常。为了解决这个问题,PGOS SDK 在检测到持久连接中断时会定期获取最新的组队信息(通常是每30到60秒),然后通过 OnPartyInfoUpdated 事件通知游戏。 因此,建议游戏除了监听以下表示组队信息变化的事件外,还要监听 OnPartyInfoUpdated 事件:

  • OnPartyStrategyChanged

  • OnPartyMemberJoined

  • OnPartyMemberLeft

  • OnPartyLeaderTransferred

  • OnCustomDataChanged

4. 关键字段和数据

字段说明Remark
Party Id组队IDParty Id 全局唯一
Name队伍名字可选且不唯一
Leader Player IdLeader的 玩家ID
Max Player最大玩家数
Created Time组队创建时间
Dismissed Time组队解散事件
Members所有组队成员的ID列表包括 leader

5. 关键错误处理

Error Code相关 API处理建议
kBackendPartyDuplicatedCreateParty JoinParty这表示玩家已经在其他组队中,因为一个玩家同时只能创建/加入一个组队。游戏可以向玩家提示类似"请先退出当前组队后再试"的信息
kBackendCreatePartyUicCreateParty这表示参数 party name 包含不当用语。游戏可以向玩家提示类似"组队名称包含不当用语,请修改后重试"的信息
kBackendIsNotPartyLeaderSetGlobalCustomData这表示该玩家不是组队的所有者却试图设置房间的全局自定义数据。此类操作是不被允许的
kBackendJoinFailedDueToNotFriendOfLeaderJoinParty这表示组队的 join_strategy 设置为 OnlyFriendsOfLeader,而尝试加入的玩家不是队长的好友。
kBackendJoinFailedDueToNotFriendOfmembersJoinParty这表示组队的 join_strategy 设置为 OnlyFriendsOfMembers,而尝试加入的玩家与组队中的任何成员都不是好友关系。
kBackendJoinFailedDueToNoInvitationJoinParty这表示组队的 join_strategy 被设置为 OnlyInvited(仅限邀请),而加入的玩家未被邀请,或者最后一次邀请已经过期。
过期时间由 PGOS 后台系统内部控制。
kBackendNoJoinPartyPermissionJoinParty这通常意味着加入的玩家被组队队长屏蔽了。
或者是加入的玩家屏蔽了组队队长,这是一个双向的关系。
kBackendPartyNotAllowJoinJoinParty这表示组队的 join_strategy 设置为 NotAllowed,因此不允许加入此组队。
kBackendInviteFailedDueToNotLeaderInvitePlayerToParty这表示组队的 invite_strategy 设置为 OnlyLeaderCanInvite,而发出邀请的玩家不是队长。
kBackendInviteFailedDueToNotAllowJoinInvitePlayerToParty这表示组队的 join_strategy 被设置为 NotAllowed,因此您无法邀请其他玩家加入此组队。