跳到主要内容

组队

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. Key Fields and Data

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

5. Key Errors Handling

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,因此您无法邀请其他玩家加入此组队。