跳到主要内容

世界频道

1. 概述

世界频道是一项允许所有玩家加入同一个全球聊天频道并自由交谈的服务。它类似于一个非常大的聊天室。通过此功能,玩家可以发送自由聊天消息,或向其他在线玩家发送组队/群组/房间邀请。对于游戏运营者来说,他们还可以向门户网站上的所有玩家广播系统通知消息。

完成本教程后,开发人员将了解:

  • PGOS世界频道的基本概念
  • 如何加入/离开世界频道
  • 如何发送和接收世界频道消息
  • 世界频道事件介绍及如何监控

2. 流程

以下是使用世界频道的流程:

sequenceDiagram participant J as Client(Jerry) participant B as PGOS Backend participant P as PGOS Portal participant O as Others on Same Channel J->>B:JoinPublicChat(channel) B-->>J:return channel info P->>B:game admin send a system message B-->>J:Event(OnReceivePublicChatMsg):system msg B-->>O:Event(OnReceivePublicChatMsg):system msg J->>B:SendPublicChatTextMsg/SendPublicChatCustomMsg B-->>O:Event(OnReceivePublicChatMsg):text/custom msg from Jerry J->>B:LeavePublicChat(channel) B-->>J:return leave result

3. 定义

  • 世界频道: 世界聊天的频道。目前每个游戏区服仅有一个可用频道,该频道名称为 Default
  • 世界子频道: 该频道会被 PGOS 后台自动分成多个子频道。玩家加入世界频道后将只会被分配到其中一个子频道中。具体机制将在下文介绍。
  • 系统消息: 游戏管理员可以在管理后台发送系统消息。这些消息将会逐步广播给频道内的所有人。

##4 关键机制

###4.1 子频道

为什么要设置子频道? 所有玩家都可以加入同一个频道。一般来说,游戏客户端会自动默认加入频道(取决于游戏设计)。因此,如果大规模玩家加入(例如>100万),我们将面临两个关键问题:

  • 后端性能问题:一旦有人在频道中发送消息,其他所有人都需要收到该消息的通知。这将给服务器带来消息广播性能问题。同时,这对网络带宽也是一个挑战。这些问题将给游戏运营带来高成本问题。
  • 用户体验问题:对于超过100万玩家的情况,每秒只有0.01%的发送率。其他所有人每秒都会收到超过100条消息。每个人都无法在屏幕上清楚地看到消息,并会说:“哦,救救我的眼睛!”。这对玩家和游戏来说真是一场灾难。

为了解决这些问题,PGOS自动将玩家分成不同的子频道。玩家只能收到同一个子频道的消息,看不到其他子频道的消息。这样一来,前面提到的两个问题就迎刃而解了。

对于玩家来说,他们看不到、认不出子频道,就认为自己加入了一个频道。对于游戏开发者来说,他们不需要处理任何子频道的事情。所有子频道的事情都由 PGOS 后端处理。

PGOS 会控制子频道的划分,以保证子频道不会超载。目前,一个子频道最多只能容纳 10000 名玩家。另一方面,如果某些子频道的玩家太少,它们将被合并为一个子频道。游戏开发者不需要关心这些事情是如何发生的、何时发生的,只需要处理消息的发送和接收。

image-20220714191853746

提示

在底层,PGOS使用桶(bucket)来存放服务器中的玩家。玩家一旦加入后,所在的桶就不会改变。子频道的分配和划分都发生在桶这一层级。如果您对子频道的算法不感兴趣,可以忽略这些细节,直接使用API即可。

4.1 系统消息

游戏管理员可以在门户网站上发送系统消息。该消息将逐步广播给频道上的所有人。通过此功能,游戏管理员可以向所有在线玩家发送全局通知。即使玩家在系统消息发送后才加入频道,他们也会收到这些系统消息。

系统消息包含以下关键字段:

  • 内容:消息的内容
  • 持续时间:以秒为单位的字段,表示系统消息将在多长时间后过期。只要消息未过期,玩家就会收到在他们加入之前发送的系统消息。

5. 使用世界频道

5.1 在门户网站上配置

如截图所示,您可以启用或禁用游戏区服的世界频道,查看子频道状态和系统消息列表,还可以添加系统消息。

image-20220714192728926

5.2 加入频道

在调用其他世界频道功能之前,您必须先加入一个频道,否则这些功能将无法使用。您可以通过调用 JoinPublicChat API 来加入频道。

频道名称可以从门户网站获取,目前只有 Default 这个名称是有效的。

接口原型:

/**
* Join a public chat.
*
* @param ChannelName The name of the channel to join which can be retrieved from the portal, currently only the name `Default` is valid.
*/
void JoinPublicChat(
const FString& ChannelName,
TFunction<void(const FPgosResult& Ret, const FClientJoinPublicChatResult* Data)> Callback) const;

示例代码:

#include "PgosSDKCpp.h"

void SomeUObjectClass::SomeFunction()
{
auto PublicChat = IPgosSDKCpp::Get().GetClientPublicChatAPI();
if (PublicChat)
{
FString ChannelName;
PublicChat->JoinPublicChat(ChannelName, [](const FPgosResult& Ret, const FClientJoinPublicChatResult* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("JoinPublicChat Successfully"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("JoinPublicChat Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

5.3 退出频道

如果不需要在某个世界频道中继续聊天,您可以通过调用 LeavePublicChat API 来退出当前频道。

当玩家离线时,PGOS 会自动将其从已加入的世界频道中移除。

接口原型:

/**
* Leave a public chat.
*
* @param ChannelName The channel to leave.
*/
void LeavePublicChat(
const FString& ChannelName,
TFunction<void(const FPgosResult& Ret)> Callback) const;

示例代码:

#include "PgosSDKCpp.h"

void SomeUObjectClass::SomeFunction()
{
auto PublicChat = IPgosSDKCpp::Get().GetClientPublicChatAPI();
if (PublicChat)
{
FString ChannelName;
PublicChat->LeavePublicChat(ChannelName, [](const FPgosResult& Ret) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("LeavePublicChat Successfully"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("LeavePublicChat Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

5.4 发送世界频道消息

加入世界频道后,玩家可以向频道发送消息。消息发送后,同一频道的其他在线玩家将会收到该消息。

发送消息有两种方式:

  • SendPublicChatTextMsg: 发送纯文本消息。消息的 msg_typeMsgTypeText,消息的文本内容存储在 content.text_content 中。
  • SendPublicChatCustomMsg: 用于发送游戏自定义消息。消息的 msg_typeMsgTypeCustom,消息内容存储在 content.custom_content 中。

这里需要注意输入参数。以 SendPublicChatTextMsg 接口的参数 FClientSendPublicChatMsgParams 为例:

struct FClientSendPublicChatMsgParams
{
/** The name of the channel on which the message was sent. */
FString channel_name;
/** The content of the message to be sent. */
FString content;
/** Custom data for message. */
FString custom_data;
/**
* The mentioned player list.
* The mentioned players will receive the corresponding events. When the value of player_id is '@ALL', it means mentioning all players.
*/
TArray<FClientMentionedPlayer> mentioned_players;
};

content is generally the input content of the user, which will be filtered by content moderation. custom_data is used for additional information added by game developers and will not be filtered by content moderation. The caveat here is not to store user input in custom_data. mentioned_players is optional, and used to mention specified players. Player_id of FClientMentionedPlayer is "@ALL" to mention all players. The mentioned players will receive the OnPublicChatNewMentioned event.

Interface prototype:

/**
* Send public chat text message.
*
* @param Params Request parameters for sending text message.
*/
void SendPublicChatTextMsg(
const FClientSendPublicChatMsgParams& Params,
TFunction<void(const FPgosResult& Ret, const FClientSendPublicChatMsgResult* Data)> Callback) const;

/**
* Send public chat custom message.
*
* @param Params Request parameters for sending custom message.
*/
void SendPublicChatCustomMsg(
const FClientSendPublicChatMsgParams& Params,
TFunction<void(const FPgosResult& Ret, const FClientSendPublicChatMsgResult* Data)> Callback) const;

示例代码:

#include "PgosSDKCpp.h"

void SomeUObjectClass::SomeFunction()
{
auto PublicChat = IPgosSDKCpp::Get().GetClientPublicChatAPI();
if (PublicChat)
{
FClientSendPublicChatMsgParams Params;
Params.channel_name = FString("Default");
Params.content = FString("hello");
Params.custom_data = FString("");
FClientMentionedPlayer MentionedAll;
MentionedAll.player_id = FString("@ALL"); // mention all members in the channel
Params.mentioned_players.Add(MentionedAll);
PublicChat->SendPublicChatTextMsg(Params, [](const FPgosResult& Ret, const FClientSendPublicChatMsgResult* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("SendPublicChatTextMsg Successfully"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("SendPublicChatTextMsg Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

void SomeUObjectClass::SomeFunction2()
{
auto PublicChat = IPgosSDKCpp::Get().GetClientPublicChatAPI();
if (PublicChat)
{
FClientSendPublicChatMsgParams Params;
Params.channel_name = FString("Default");
Params.content = FString("hello");
Params.custom_data = FString("");
FClientMentionedPlayer MentionedPlayer;
MentionedPlayer.player_id = FString("123456"); // mention player "123456"
Params.mentioned_players.Add(MentionedPlayer);
PublicChat->SendPublicChatCustomMsg(Params, [](const FPgosResult& Ret, const FClientSendPublicChatMsgResult* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("SendPublicChatCustomMsg Successfully"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("SendPublicChatCustomMsg Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

5.5 Receiving the Messages

OnReceivePublicChatMsg event will be triggered when receiving public chat messages. The event results are saved in FClientReceivePublicChatMsgEvt.

struct FClientReceivePublicChatMsgEvt 
{
/** Public chat message. */
FClientPublicChatMsgInfo msg;
/** Public chat channel info. */
FClientPublicChatChannelInfo channel_info;
};

Monitor events from the PublicChat module, as follows:

#include "PgosSDKCpp.h"

void SomeClass::MonitorPublicChatEvents()
{
auto PublicChat = IPgosSDKCpp::Get().GetClientPublicChatAPI();
if (PublicChat)
{
PublicChat->OnReceivePublicChatMsg().AddUObject(this, &SomeClass::OnReceivePublicChatMsg);
}
}

void SomeClass::OnReceivePublicChatMsg(const FClientReceivePublicChatMsgEvt& Event)
{
UE_LOG(LogTemp, Log, TEXT("OnReceivePublicChatMsg"));
const auto MsgType = Event.msg.msg_type;
if (MsgType == EClientChatMsgType::MsgTypeText)
{
UE_LOG(LogTemp, Log, TEXT("receive text msg, content=(%s)"), *Event.msg.content.text_content);
} else if (MsgType == EClientChatMsgType::MsgTypeCustom)
{
UE_LOG(LogTemp, Log, TEXT("receive custom msg, content=(%s)"), *Event.msg.content.custom_content);
} else if (MsgType == EClientChatMsgType::MsgTypeSystem)
{
UE_LOG(LogTemp, Log, TEXT("receive system msg, content=(%s), duration_sec=(%d)"), *Event.msg.content.system_content.msg, Event.msg.content.system_content.duration_sec);
}
}

5.6 Mention Players

Specified players can be mentioned when sending a message. For details, please view Send Public Chat Messages

If someone in the channel mentioned you, you will receive the OnPublicChatNewMentioned event.

You can query the last mentioned record of the specified channel by calling the interface GetPublicChatMentionedInfo.

You can clear the records of the specified channel mentioned to you by calling the interface ClearPublicChatMentionedInfo. When channel_name is empty, it means clearing the mentioned records of all channels.

Interface prototype:

/**
* Get latest record mentioning me in the channel.
*
* @param ChannelName The name of the channel.
* @param Dst The latest record mentioned me.
* @return True if there are any mentions of me.
*/
bool GetPublicChatMentionedInfo(
const FString& ChannelName,
FClientPublicChatMentionedInfo& Dst) const;

/**
* Clear record mentioning me in the channel.
*
* @param ChannelName The name of the channel. If empty, it means all channels.
*/
void ClearPublicChatMentionedInfo(const FString& ChannelName) const;

示例代码:

#include "PgosSDKCpp.h"

void SomeUObjectClass::SomeFunction1()
{
auto PublicChat = IPgosSDKCpp::Get().GetClientPublicChatAPI();
if (PublicChat)
{
FString ChannelName = TEXT("Default");
FClientPublicChatMentionedInfo Dst;
PublicChat->GetPublicChatMentionedInfo(ChannelName, Dst);
if (result)
{
UE_LOG(LogTemp, Log, TEXT("%s mentioned you"), *Dst.msg.sender.display_name);
}
else
{
UE_LOG(LogTemp, Log, TEXT("No one mentioned you"));
}
}
}

void SomeUObjectClass::SomeFunction2()
{
auto PublicChat = IPgosSDKCpp::Get().GetClientPublicChatAPI();
if (PublicChat)
{
FString ChannelName = TEXT("Default");
PublicChat->ClearPublicChatMentionedInfo(ChannelName);
}
}

void SomeClass::MonitorGroupChatEvents()
{
auto PublicChat = IPgosSDKCpp::Get().GetClientPublicChatAPI();
if (PublicChat)
{
PublicChat->OnPublicChatNewMentioned().AddUObject(this, &SomeClass::OnPublicChatNewMentioned);
}
}

void SomeClass::OnPublicChatNewMentioned(const FClientPublicChatNewMentionedEvt& Event)
{
UE_LOG(LogTemp, Log, TEXT("OnPublicChatNewMentioned"));
}

6 Key Errors Handling

Error Code相关 API处理建议
kBackendPubChatChannelNameInvalidJoinPublicChat这表示频道名称无效,您可以从门户网站获取频道名称
kBackendPubChatChannelDisabledJoinPublicChat
SendPublicChatTextMsg
SendPublicChatCustomMsg
这表示该频道已被禁用。
kCltSdkPublicChatNeedJoinFirstSendPublicChatTextMsg
SendPublicChatCustomMsg
这表示该用户尚未加入频道。
kBackendIMChatFrequencyLimitSendPublicChatTextMsg
SendPublicChatCustomMsg
这表示操作频率超出限制。建议采用指数递增的时间间隔重试:5秒、10秒、20秒等。
kBackendIMChatMsgIsTooLongSendPublicChatTextMsg
SendPublicChatCustomMsg
这表示消息内容过长。私聊/群组聊天的字符限制为2000个字符,世界频道的字符限制为500个字符。游戏应该向玩家提示"消息过长,请缩短内容或将其分成多条消息发送。"
kBackendIMChatCustomDataIsTooLongSendPublicChatTextMsg
SendPublicChatCustomMsg
这表示自定义数据过长。个人/群组聊天的限制为8000字节,世界频道的限制为1000字节。游戏会捕获此错误并避免使用过长的自定义数据。
kBackendProfaneWordsSendPublicChatTextMsg
SendPublicChatCustomMsg
这表示消息内容包含不当用语且无法被屏蔽。游戏应该向玩家提示类似"请删除不当用语后再发送消息"的提示。
kBackendErrPlayerIsPunishedSendPublicChatTextMsg
SendPublicChatCustomMsg
该玩家受到惩罚。msg是一个JSON序列化字符串,包含截止时间(惩罚结束时的Unix时间戳(以秒为单位),小于等于0表示永久惩罚)和原因。