跳到主要内容

Personal Chat

1. 概述

本文档介绍了游戏内私聊功能的使用方法,用于与其他玩家进行一对一聊天。通过提供对方玩家的ID即可发起私聊。

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

  • PGOS私聊的基本概念
  • 私聊的典型使用场景
  • 如何发送和接收私聊消息
  • 如何获取最近聊天列表以及如何获取与特定玩家的历史消息
  • 私聊事件介绍及其监听方法

2. 流程

以下是使用私聊功能的流程:

img

私聊互动流程如下:

  1. 成功登录后,私聊入口图标会显示未读私聊消息数量。
  2. 点击私聊图标打开聊天窗口。
  3. 窗口首先显示最近的私聊会话列表。
  4. 选择一个会话以显示与相应玩家的聊天内容。
  5. 与该玩家进行聊天。

img

3. 数据结构

私聊使用 FClientPersonalMsgInfoFClientPersonalChatItem 数据结构。FClientPersonalMsgInfo 用于显示消息内容,FClientPersonalChatItem 用于显示玩家对话。

struct FClientPersonalMsgInfo 
{
/** The player who sent the message */
FPlayerInfo sender;
/** The time the message was sent */
int64 sent_time;
/** The type of the message */
EClientChatMsgType msg_type;
/** The message to be sent */
FClientPersonalMsgContent content;
/** The sequence number of the message */
int64 seq;
/** Additional information added by the game */
FString custom_data;
};

struct FClientPersonalChatItem
{
/** The peer player info */
FPlayerInfo peer_player_info;
/** Unread message count */
int32 unread_count;
/** Recent chat message info */
FClientPersonalMsgInfo msg_latest;
/** Is the player my friend */
bool is_friend;
};

enum class EClientChatMsgType : uint8
{
MsgTypeText = 0 UMETA(DisplayName = "MsgTypeText"),
MsgTypeCustom = 1 UMETA(DisplayName = "MsgTypeCustom")
};

struct FClientPersonalMsgContent
{
/** Take the value from here when msg type is MsgTypeText */
FString text_content;
/** Take the value from here when msg type is MsgTypeCustom */
FString custom_content;
};

EClientChatMsgType 类型对应 FClientPersonalMsgContent 中的字段,目前 EClientChatMsgType 包括 MsgTypeTextMsgTypeCustom,当 EClientChatMsgTypeMsgTypeText 时,消息内容在 FClientPersonalMsgContenttext_content 中,当 EClientChatMsgTypeMsgTypeCustom 时,消息内容在 FClientPersonalMsgContentcustom_content 中。

除了以上数据结构,你可能还想了解以下几个概念:

  • 私聊存储
    • 后端存储:后端会为每个玩家最多商店 50 条未读消息,若未读消息超过 50 条,则会清除最旧的消息,未读消息暂存时长最长为 7 天;但当有新的未读消息到达时,清理倒计时会重置,也就是说,如果有新的未读消息,临时存储会再保留 7 天。
    • 本地存储: 私聊中发送和接收的消息都存储在客户端的本地数据库中,每个账号使用单独的数据库文件保存私聊消息,你可以在 pgos_config.ini 中配置 chat_db_max_bytes 值来限制单个数据库文件的最大大小。我们还提供了 GetLocalCacheSize获取数据库文件大小和 ClearLocalCache 的 API#312-clearlocalcache-client-api)清除聊天记录的 API。您可以使用这些 API 在数据库文件过大时提示用户清除聊天记录。
  • 私聊会话:跟踪与其他玩家的私聊互动。
  • seq:随着玩家从给定玩家收到越来越多的消息,消息的序列号会增加。
  • 未读私聊消息:未标记为已读的私聊消息。

注意: chat_db_max_bytes 的最小值为 1MB(1048576),不建议设置超过 100MB 的值。如果您未指定值,则采用默认值 100MB。 当单个数据库文件超出 chat_db_max_bytes 的值时,PGOS 会自动删除较旧的消息以释放空间。

4. 使用私聊

4.1 获取最近私聊会话列表

当玩家准备发起私聊时,可能需要使用 GetMyPersonalChatList API 来查看最近的私聊会话列表。

/**
* Get my personal chat list
*/
void GetMyPersonalChatList(TFunction<void(const FPgosResult& Ret, const FClientGetMyPersonalChatListResult* Data)> Callback) const;

返回的结果保存在FClientGetMyPersonalChatListResult结构中。返回值列表使用以下优先级排序:未读消息数量 > 最后一条消息的时间。

struct PGOSSDKCPP_API FClientGetMyPersonalChatListResult 
{
/** Personal chat list */
TArray<FClientPersonalChatItem> personal_chat_list;
};

struct PGOSSDKCPP_API FClientPersonalChatItem
{
/** The peer player info */
FPlayerInfo peer_player_info;
/** Unread message count */
int32 unread_count;
/** Recent chat message info */
FClientPersonalMsgInfo msg_latest;
/** Is the player my friend */
bool is_friend;
};

按照以下方式从 PersonalChat 模块调用 GetMyPersonalChatList

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

void SomeClass::GetMyPersonalChatList()
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->GetMyPersonalChatList([](const FPgosResult& Ret, const FClientGetMyPersonalChatListResult* Result)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("GetMyPersonalChatList Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("GetMyPersonalChatList Failed: err_code=(%d), err_msg=(%s)"), Ret.err_code,
*Ret.msg);
}
});
}
}

4.2 获取私聊消息列表

PGOS在本地存储私聊消息。您可以使用GetPersonalMsgList分页查询功能来获取指定玩家的聊天记录。

/**
* Get chat message list with a player
*
* @param PeerPlayerId The peer player ID
* @param StartSeq The start sequence number of the message to query, 0 means start from the latest message.
* @param Count The count of message to get
*/
void GetPersonalMsgList(
const FString& PeerPlayerId,
int64 StartSeq,
int32 Count,
TFunction<void(const FPgosResult& Ret, const FClientGetPersonalChatMsgListResult* Data)> Callback) const;

返回的结果保存在 FClientGetPersonalChatMsgListResult 结构体中:

struct PGOSSDKCPP_API FClientGetPersonalChatMsgListResult 
{
/** Chat message info list */
TArray<FClientPersonalMsgInfo> msg_list;
/** Whether has more message */
bool has_more;
/** The starting sequence for the next query */
int64 next_seq;
};

本地存储和获取聊天记录的机制如下图所示:

img

按照以下方式从 PersonalChat 模块调用 GetPersonalMsgList

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

void SomeClass::GetPersonalMsgList(const FString& PlayerId, int64 StartSeq)
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->GetPersonalMsgList(PlayerId, StartSeq, 20, [](const FPgosResult& Ret, const FClientGetPersonalChatMsgListResult* Result)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("GetPersonalMsgList Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("GetPersonalMsgList Failed: err_code=(%d), err_msg=(%s)"), Ret.err_code,
*Ret.msg);
}
});
}
}

4.3 发送私聊消息

所有玩家都可以使用玩家ID与对应的玩家发起私聊。发送私聊消息后,如果对方在线,将立即收到该消息。

发送消息有两种方式:

  • SendPersonalTextMsg: 发送简单的文本消息。消息的 msg_typeMsgTypeText,消息的文本内容存储在 content.text_content 中。
  • SendPersonalCustomMsg: 用于发送游戏自定义消息。消息的 msg_typeMsgTypeCustom,消息内容存储在 content.custom_content 中。
/**
* Send personal chat text message
*
* @param TextMsgReq Request struct for sending text message
*/
void SendPersonalTextMsg(
const FClientSendPersonalMsgParams& Params,
TFunction<void(const FPgosResult& Ret, const FClientPersonalMsgInfo* Data)> Callback) const;

/**
* Send personal chat custom message
*
* @param CustomMsgReq Request struct for sending custom message
*/
void SendPersonalCustomMsg(
const FClientSendPersonalMsgParams& Params,
TFunction<void(const FPgosResult& Ret, const FClientPersonalMsgInfo* Data)> Callback) const;

在此您需要注意输入参数。以SendPersonalTextMsg接口的参数FClientPersonalTextMsgReq为例:

struct FClientSendPersonalMsgParams 
{
/** Peer player's ID */
FString peer_player_id;
/** content */
FString content;
/** Additional information added by the game */
FString custom_data;
};

content 通常是用户的输入内容,会经过内容审核过滤。custom_data 用于游戏开发者添加的额外信息,不会经过内容审核过滤。这里需要注意的是不要在 custom_data 中存储用户输入。 返回结果保存在 FClientPersonalMsgInfo 结构体中,该结构体在上文已详细描述。 从 PersonalChat 模块调用 SendPersonalTextMsg/SendPersonalCustomMsg 的方式如下:

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

void SomeClass::SendPersonalTextMsg(const FString& PlayerId, const FString& Message)
{
FClientSendPersonalMsgParams Params;
Params.player_id = PlayerId;
Params.content = Message;
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->SendPersonalTextMsg(Params, [](const FPgosResult& Ret, const FClientPersonalMsgInfo* Info)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("SendPersonalTextMsg Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("SendPersonalTextMsg Failed: err_code=(%d), err_msg=(%s)"), Ret.err_code,
*Ret.msg);
}
});
}
}

void SomeClass::SendPersonalCustomMsg(const FString& PlayerId, const FString& CustomMessage)
{
FClientSendPersonalMsgParams Params;
Params.player_id = PlayerId;
Params.content = Message;
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->SendPersonalCustomMsg(Params, [](const FPgosResult& Ret, const FClientPersonalMsgInfo* Info)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("SendPersonalCustomMsg Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("SendPersonalCustomMsg Failed: err_code=(%d), err_msg=(%s)"), Ret.err_code,
*Ret.msg);
}
});
}
}

注意:玩家可以选择是否允许非好友向其发送私聊消息。详见隐私设置文档

###4.4 激活私聊

可以通过ActivePersonalChat API开启与指定玩家的私聊 。 正常情况下,玩家在与其他玩家聊天时会不断收到消息,每收到一条消息,未读消息数都会发生变化,触发OnUnreadPersonalMsgCountNotify 事件和OnReceivePersonalMsg 事件 ,通知该玩家未读消息总数和未读消息数的变化。 该玩家正与其他玩家处于聊天界面,所以有新消息应该自动标记为已读。 为了避免未读消息总数反复发生变化而再次触发OnUnreadPersonalMsgCountNotify 事件 ,我们提供了ActivePersonalChat API,用于开启与指定玩家的私聊。

  • 开启前,每收到一条新消息,未读消息数都会发生变化,触发OnUnreadPersonalMsgCountNotify 事件 。 - 激活后,指定玩家的未读消息数将保持为0,因此新消息不会触发OnUnreadPersonalMsgCountNotify事件。

如下图示例,您当前正在与Jerry聊天,当您收到Jerry的消息时,默认情况下会将其标记为已读,并且不会改变未读消息总数。当您与Jerry处于聊天界面时,ActivePersonalChat API会激活聊天,并且Jerry的未读消息数保持为0,因此当您收到Jerry的私聊消息时,不会触发OnUnreadPersonalMsgCountNotify 事件 。当您离开聊天界面时,会向API传递一个空的PlayerId参数,这样就不会激活私聊 。

img

/**
* Set the current personal chat. When setting, OnUnreadPersonalMsgCountNotify will be pushed, and subsequent personal chat messages currently in progress are automatically marked as read
*
* @param PlayerId The player to set as active
*/
void ActivePersonalChat(const FString& PlayerId) const;

按照以下方式调用 PersonalChat 模块中的 ActivePersonalChat

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

void SomeClass::ActivePersonalChat(const FString& PlayerId)
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->ActivePersonalChat(PlayerId);
}
}

注意:

  • 同一时间只能激活一个聊天。
  • PlayerId为空时,表示没有激活的会话。

4.5 清除未读私聊消息数

如果来自指定玩家的所有私聊消息都已读,您可以使用ClearPersonalUnreadCount API将该玩家的未读消息数设置为零。

/**
* Mark messages with someone as all read
*
* @param PlayerId is the ID of the specified player. If it is null, the unread message counts for all current players are set to zero.
*/
void ClearPersonalUnreadCount(
const FString& PlayerId,
TFunction<void(const FPgosResult& Ret) const;

按照以下方式调用 PersonalChat 模块中的 ClearPersonalUnreadCount

清零未读私聊消息数量

如果来自指定玩家的所有私聊消息都已读,您可以使用 ClearPersonalUnreadCount API 将该玩家的未读消息数量设置为零。

/**
* Mark messages with someone as all read
*
* @param PlayerId is the ID of the specified player. If it is null, the unread message counts for all current players are set to zero.
*/
void ClearPersonalUnreadCount(
const FString& PlayerId,
TFunction<void(const FPgosResult& Ret, const FClientClearPersonalUnreadCountResult* Data)> Callback) const;

按照以下方式调用 PersonalChat 模块中的 ClearPersonalUnreadCount

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

void SomeClass::ClearPersonalUnreadCount(const FString& PlayerId)
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->ClearPersonalUnreadCount(PlayerId, [](const FPgosResult& Ret, const FClientClearPersonalUnreadCountResult* Result)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("ClearPersonalUnreadCount Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("ClearPersonalUnreadCount Failed: err_code=(%d), err_msg=%s]"), Ret.err_code,
*Ret.msg);
}
});
}
}

4.6 监听私聊事件

使用私聊时会触发以下事件:

  • OnUnreadPersonalMsgCountNotify: 登录后立即触发,以及未读消息总数发生变化时触发。事件结果保存在 FClientUnreadMsgCountNotifyEvt 中。
  • OnReceivePersonalMsg: 接收到私聊消息时触发。事件结果保存在 FClientReceivePersonalMsgEvt 中。
struct PGOSSDKCPP_API FClientUnreadMsgCountNotifyEvt 
{
/** Total unread count of personal chat or group chat */
int32 total_unread_count;
};

struct PGOSSDKCPP_API FClientReceivePersonalMsgEvt
{
/** Personal chat message received */
FClientPersonalMsgInfo msg;
/** Count of unread message from sender */
int32 unread_count;
};

按照以下方式监听来自 PersonalChat 模块的私聊事件:

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

void SomeClass::MonitorPersonalChatEvents()
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->OnUnreadPersonalMsgCountNotify().AddUObject(this, &SomeClass::OnUnreadPersonalMsgCountNotify);
PersonalChat->OnReceivePersonalMsg().AddUObject(this, &SomeClass::OnReceivePersonalMsg);
}
}

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

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

5. 关键错误处理

Error Code相关API处理建议
kBackendIMChatFrequencyLimitSendPersonalTextMsg SendPersonalCustomMsg这表示操作频率超出限制。建议采用指数级递增的时间间隔重试:5秒、10秒、20秒等。
kBackendIMChatMsgIsTooLongSendPersonalTextMsg SendPersonalCustomMsg这表示消息内容过长。消息长度限制为1000个字符。游戏应该向玩家显示提示信息,比如"消息过长,请缩短内容或拆分为多条消息发送。"
kBackendIMChatCustomDataIsTooLongSendPersonalTextMsg SendPersonalCustomMsg这表示自定义数据太长,限制是8000字节,游戏会捕获这个错误,避免使用超长的自定义数据。
kBackendProfaneWordsSendPersonalTextMsg SendPersonalCustomMsg这表示消息内容包含不当用语且无法被屏蔽。游戏应该向玩家提示类似"请删除不当用语后再发送消息"的提示。
kBackendErrPlayerIsPunishedSendPersonalTextMsg SendPersonalCustomMsg玩家受到惩罚。msg是一个JSON序列化字符串,包含截止时间(惩罚结束时的Unix时间戳(以秒为单位),小于等于0表示永久惩罚)和原因。