Implement Subsystem - Private Chat - (Unreal Engine module)
While you don't have to set anything up in the Admin Portal to use the private chat feature, there are some parameters you can change there if you want, such as character limit, spam protection, etc. Refer to Introduction to Chat for more information.
Unwrap the Subsystem
In this tutorial, you will learn how to implement private chat using the AGS Online Subsystem (OSS). In the Byte Wars project, a game instance subsystem named PrivateChatSubsystem
has already been created. This subsystem contains functions related to private chat. In this tutorial, you will use a starter version of that subsystem to implement private chat from scratch.
What's in the Starter Pack
To follow this tutorial, a starter subsystem class named PrivateChatSubsystem_Starter
has been prepared for you. This class consists of the following files:
- Header file:
/Source/AccelByteWars/TutorialModules/Social/PrivateChat/PrivateChatSubsystem_Starter.h
- CPP file:
/Source/AccelByteWars/TutorialModules/Social/PrivateChat/PrivateChatSubsystem_Starter.cpp
The PrivateChatSubsystem_Starter
class provides several functions, including:
-
An AGS OSS chat interface that you can use to implement private chat-related functions:
FOnlineChatAccelBytePtr UPrivateChatSubsystem_Starter::GetChatInterface() const
{
const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
if (!ensure(Subsystem))
{
UE_LOG_PRIVATECHAT(Warning, TEXT("The online subsystem is invalid. Please make sure OnlineSubsystemAccelByte is enabled and the DefaultPlatformService under [OnlineSubsystem] in the Engine.ini file is set to AccelByte."));
return nullptr;
}
return StaticCastSharedPtr<FOnlineChatAccelByte>(Subsystem->GetChatInterface());
}FOnlineIdentityAccelBytePtr UPrivateChatSubsystem_Starter::GetIdentityInterface() const
{
const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
if (!ensure(Subsystem))
{
UE_LOG_PRIVATECHAT(Warning, TEXT("The online subsystem is invalid. Please make sure OnlineSubsystemAccelByte is enabled and DefaultPlatformService under [OnlineSubsystem] in the Engine.ini set to AccelByte."));
return nullptr;
}
return StaticCastSharedPtr<FOnlineIdentityAccelByte>(Subsystem->GetIdentityInterface());
} -
A utility to prompt notifications to the game UI. You will need this to display notifications when the player receives new private chat messages.
UPromptSubsystem* UPrivateChatSubsystem_Starter::GetPromptSubsystem() const
{
const UAccelByteWarsGameInstance* GameInstance = Cast<UAccelByteWarsGameInstance>(GetGameInstance());
if (!GameInstance)
{
return nullptr;
}
return GameInstance->GetSubsystem<UPromptSubsystem>();
} -
A function to demonstrate how to push a notification to the Byte Wars notification system. It takes the player's name and displays a notification stating that a message from that player was received.
void UPrivateChatSubsystem_Starter::PushPrivateChatMessageReceivedNotification(const FUniqueNetId& UserId, const TSharedRef<FChatMessage>& Message)
{
if (!GetPromptSubsystem())
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot push private chat message received notification. Prompt Subsystem is not valid."));
return;
}
const FUniqueNetIdAccelByteUserRef SenderABId = StaticCastSharedRef<const FUniqueNetIdAccelByteUser>(Message->GetUserId());
if (!SenderABId.Get().IsValid())
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot push private chat message received notification. Sender User Id is not valid."));
return;
}
// Only push a notification only if the player is not in the chat menu of the same recipient.
const UCommonActivatableWidget* ActiveWidget = UAccelByteWarsBaseUI::GetActiveWidgetOfStack(EBaseUIStackType::Menu, this);
if (const UPrivateChatWidget_Starter* PrivateChatWidget = Cast<UPrivateChatWidget_Starter>(ActiveWidget))
{
const FUniqueNetIdAccelByteUserPtr CurrentRecipientABId = StaticCastSharedPtr<const FUniqueNetIdAccelByteUser>(PrivateChatWidget->GetPrivateChatRecipient());
if (!CurrentRecipientABId || CurrentRecipientABId->GetAccelByteId().Equals(SenderABId->GetAccelByteId()))
{
return;
}
}
// If the sender doesn't have display name, then use the default display name.
FString SenderStr = Message.Get().GetNickname();
if (SenderStr.IsEmpty() || SenderABId.Get().GetAccelByteId().Equals(SenderStr))
{
SenderStr = UTutorialModuleOnlineUtility::GetUserDefaultDisplayName(Message->GetUserId().Get());
}
GetPromptSubsystem()->PushNotification(FText::Format(PRIVATE_CHAT_RECEIVED_MESSAGE, FText::FromString(SenderStr)));
}
Implement connect chat
In this section, you will implement functions to connect and reconnect the chat if it gets disconnected.
-
First, open the
DefaultEngine.ini
file and make sure this option is enabled. This will tell the AccelByte OSS to auto-connect to the chat after a successful login.[OnlineSubsystemAccelByte]
bAutoChatConnectAfterLoginSuccess=true -
Open the
PrivateChatSubsystem_Starter
class header file and declare the following variable to store the number of reconnect retries:private:
// ...
int32 ReconnectChatNumTries = 0;
const int32 ReconnectChatMaxTries = 3; -
Next, declare a new function to perform the chat reconnection.
protected:
// ...
void ReconnectChat(FString Message); -
Open the
PrivateChatSubsystem_Starter
class CPP file and define the function above. This function retries to connect the logged-in player to the chat within a specified maximum number of retries.void UPrivateChatSubsystem_Starter::ReconnectChat(FString Message)
{
if (!GetWorld() || GetWorld()->bIsTearingDown)
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Failed to reconnect to chat service. The level world is tearing down."));
return;
}
if (!GetChatInterface())
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Failed to reconnect to chat service. The chat interface is tearing down."));
return;
}
if (!GetIdentityInterface() || GetIdentityInterface()->GetLoginStatus(0) == ELoginStatus::Type::LoggedIn)
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Failed to reconnect to chat service. The player is not logged in yet."));
return;
}
if (ReconnectChatNumTries >= ReconnectChatMaxTries)
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Failed to reconnect to chat service. Number tries to reconnect chat reached %d times limit."), ReconnectChatMaxTries);
return;
}
UE_LOG_PRIVATECHAT(Log, TEXT("Try to reconnect chat due to: %s"), *Message);
GetChatInterface()->Connect(0);
ReconnectChatNumTries++;
} -
Then, you need to bind the function above so that it will be called when the private chat message is disconnected. You can do this by adding the highlighted code below to the
Initialize()
function.void UPrivateChatSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
if (GetChatInterface())
{
// ...
// Try reconnect chat on disconnected from chat.
GetChatInterface()->OnConnectChatCompleteDelegates->AddWeakLambda(this, [this](int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& ErrorMessage)
{
if (bWasSuccessful)
{
UE_LOG_PRIVATECHAT(Log, TEXT("Success to connect to chat service."));
ReconnectChatNumTries = 0;
return;
}
ReconnectChat(ErrorMessage);
});
GetChatInterface()->OnChatDisconnectedDelegates.AddUObject(this, &ThisClass::ReconnectChat);
// ...
}
} -
Finally, you need to unbind the callback function when the subsystem is deinitialized. You can do this by adding the highlighted code below to the
Deinitialize()
function:void UPrivateChatSubsystem_Starter::Deinitialize()
{
// ...
if (GetChatInterface())
{
// ...
GetChatInterface()->OnConnectChatCompleteDelegates->RemoveAll(this);
GetChatInterface()->OnChatDisconnectedDelegates.RemoveAll(this);
// ...
}
}
Implement private chat utilities
In this section, you will implement some utilities for the private chat.
-
Open the
PrivateChatSubsystem_Starter
class header file and declare the following functions:public:
// ...
FString GetPrivateChatRoomId(const FUniqueNetIdPtr SenderUserId, const FUniqueNetIdPtr RecipientUserId);
bool IsMessageFromLocalUser(const FUniqueNetIdPtr UserId, const FChatMessage& Message); -
Next, open the
PrivateChatSubsystem_Starter
class CPP file and defineGetPrivateChatRoomId()
. This function will return the private chat room ID. It can be used to differentiate between received chat types (e.g., session chat vs. private chat).FString UPrivateChatSubsystem_Starter::GetPrivateChatRoomId(const FUniqueNetIdPtr SenderUserId, const FUniqueNetIdPtr RecipientUserId)
{
if (!GetChatInterface())
{
return FString();
}
if (!SenderUserId || !RecipientUserId)
{
return FString();
}
const FUniqueNetIdAccelByteUserPtr SenderABId = StaticCastSharedPtr<const FUniqueNetIdAccelByteUser>(SenderUserId);
const FUniqueNetIdAccelByteUserPtr RecipientABId = StaticCastSharedPtr<const FUniqueNetIdAccelByteUser>(RecipientUserId);
if (!SenderABId || !RecipientABId)
{
return FString();
}
TOptional<FString> PersonalRoomId = GetChatInterface()->GetPersonalChatTopicId(SenderABId->GetAccelByteId(), RecipientABId->GetAccelByteId());
return PersonalRoomId.IsSet() ? PersonalRoomId.GetValue() : TEXT("");
} -
Still in the same file, define the
IsMessageFromLocalUser()
function. This function checks whether the chat is from the local player or other players. It can be used for displaying the chat message later by differentiating how you display received messages and sent messages (e.g., colors and alignment).bool UPrivateChatSubsystem_Starter::IsMessageFromLocalUser(const FUniqueNetIdPtr UserId, const FChatMessage& Message)
{
if (!GetChatInterface())
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot check whether chat message is from local user or not. Chat Interface is not valid."));
return false;
}
if (!UserId)
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot check whether chat message is from local user or not. User NetId is not valid."));
return false;
}
return GetChatInterface()->IsMessageFromLocalUser(UserId.ToSharedRef().Get(), Message, true);
}
Implement sending private chat messages
In this section, you will implement functions to send a private chat message.
-
Open the
PrivateChatSubsystem_Starter
class header file and declare the following function:public:
// ...
void SendPrivateChatMessage(const FUniqueNetIdPtr UserId, const FUniqueNetIdPtr RecipientUserId, const FString& Message); -
Next, declare a delegate and a callback function to handle when a private chat message is sent. You will use this delegate to connect with the private chat UI later.
public:
// ...
FOnSendChatComplete* GetOnSendPrivateChatCompleteDelegates()
{
return &OnSendPrivateChatCompleteDelegates;
}protected:
void OnSendPrivateChatComplete(FString UserId, FString MsgBody, FString RoomId, bool bWasSuccessful);private:
FOnSendChatComplete OnSendPrivateChatCompleteDelegates; -
Now, define the functions above. Open the
PrivateChatSubsystem_Starter
class CPP file and define theSendPrivateChatMessage()
function first to send a private chat message.void UPrivateChatSubsystem_Starter::SendPrivateChatMessage(const FUniqueNetIdPtr UserId, const FUniqueNetIdPtr RecipientUserId, const FString& Message)
{
if (!GetChatInterface())
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot send private chat message. Chat Interface is not valid."));
OnSendPrivateChatComplete(FString(), Message, FString(), false);
return;
}
if (!UserId)
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot send private chat message. User NetId is not valid."));
OnSendPrivateChatComplete(FString(), Message, FString(), false);
return;
}
if (!RecipientUserId)
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot send private chat message. Recipient NetId is not valid."));
OnSendPrivateChatComplete(FString(), Message, FString(), false);
return;
}
GetChatInterface()->SendPrivateChat(UserId.ToSharedRef().Get(), RecipientUserId.ToSharedRef().Get(), Message);
} -
Next, define the
OnSendPrivateChatComplete()
function. When the send private chat request is complete, it will broadcast the delegate you created earlier to inform you that the process has completed.void UPrivateChatSubsystem_Starter::OnSendPrivateChatComplete(FString UserId, FString MsgBody, FString RoomId, bool bWasSuccessful)
{
// Abort if the room id is not a private chat room.
if (!GetChatInterface() || GetChatInterface()->GetChatRoomType(RoomId) != EAccelByteChatRoomType::PERSONAL)
{
return;
}
if (bWasSuccessful)
{
UE_LOG_PRIVATECHAT(Log, TEXT("Success to send chat message on Room %s"), *RoomId);
}
else
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Failed to send chat message on Room %s"), *RoomId);
}
OnSendPrivateChatCompleteDelegates.Broadcast(UserId, MsgBody, RoomId, bWasSuccessful);
} -
Then, you need to bind the callback function above so it will be called when the private chat message is sent. You can do this by adding the highlighted code below to the
Initialize()
function, which is the first function to be called when the subsystem is initialized.void UPrivateChatSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
if (GetChatInterface())
{
GetChatInterface()->OnSendChatCompleteDelegates.AddUObject(this, &ThisClass::OnSendPrivateChatComplete);
// ...
}
} -
Finally, you need to unbind the callback function when the subsystem is deinitialized. You can do this by adding the highlighted code below to the
Deinitialize()
function:void UPrivateChatSubsystem_Starter::Deinitialize()
{
// ...
if (GetChatInterface())
{
GetChatInterface()->OnSendChatCompleteDelegates.RemoveAll(this);
// ...
}
}
Implement receiving private chat messages
In this section, you will implement functions to handle when a private chat is received.
-
Open the
PrivateChatSubsystem_Starter
class header file and declare a delegate and a callback function to handle when a private chat message is received. You will use this to show a notification when a new chat is received. You will also use this delegate to connect with the private chat UI later to show the received message.public:
// ...
FOnChatPrivateMessageReceived* GetOnPrivateChatMessageReceivedDelegates()
{
return &OnPrivateChatMessageReceivedDelegates;
}protected:
// ...
void OnPrivateChatMessageReceived(const FUniqueNetId& UserId, const TSharedRef<FChatMessage>& Message);private:
// ...
FOnChatPrivateMessageReceived OnPrivateChatMessageReceivedDelegates; -
Next, open the
PrivateChatSubsystem_Starter
class CPP file and define theOnPrivateChatMessageReceived()
function. When the private chat is received, it will broadcast the delegate you created earlier.void UPrivateChatSubsystem_Starter::OnPrivateChatMessageReceived(const FUniqueNetId& UserId, const TSharedRef<FChatMessage>& Message)
{
UE_LOG_PRIVATECHAT(Log,
TEXT("Received private chat message from %s: %s"),
!Message.Get().GetNickname().IsEmpty() ? *Message.Get().GetNickname() : *UTutorialModuleOnlineUtility::GetUserDefaultDisplayName(Message->GetUserId().Get()),
*Message.Get().GetBody());
OnPrivateChatMessageReceivedDelegates.Broadcast(UserId, Message);
} -
Then, you need to bind the callback function above so it will be called when the private chat message is received. You can do this by adding the highlighted code below to the
Initialize()
function, which is the first function to be called when the subsystem is initialized. Here, you also bind a predefined function namedPushPrivateChatMessageReceivedNotification()
to show a notification when a new chat is received.void UPrivateChatSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
if (GetChatInterface())
{
// ...
GetChatInterface()->OnChatPrivateMessageReceivedDelegates.AddUObject(this, &ThisClass::OnPrivateChatMessageReceived);
// Push a notification when received chat messages.
GetChatInterface()->OnChatPrivateMessageReceivedDelegates.AddUObject(this, &ThisClass::PushPrivateChatMessageReceivedNotification);
// ...
}
} -
Finally, you need to unbind the callback function when the subsystem is deinitialized. You can do this by adding the highlighted code below to the
Deinitialize()
:void UPrivateChatSubsystem_Starter::Deinitialize()
{
// ...
if (GetChatInterface())
{
// ...
GetChatInterface()->OnChatPrivateMessageReceivedDelegates.RemoveAll(this);
// ...
}
}
Implement getting last private chat messages
In this section, you will implement functions to get the last private chat messages.
-
Open the
PrivateChatSubsystem_Starter
class header file and declare the following function:public:
// ...
bool GetLastPrivateChatMessages(const FUniqueNetIdPtr UserId, const FChatRoomId& RoomId, const int32 NumMessages, TArray<TSharedRef<FChatMessage>>& OutMessages); -
Next, open the
PrivateChatSubsystem_Starter
class CPP file and define the function above. This function will return the last chat messages saved in the game cache.注記All of the chat messages are saved in the game cache at runtime. When you close the game, the chat messages will be deleted.
bool UPrivateChatSubsystem_Starter::GetLastPrivateChatMessages(const FUniqueNetIdPtr UserId, const FChatRoomId& RoomId, const int32 NumMessages, TArray<TSharedRef<FChatMessage>>& OutMessages)
{
if (!GetChatInterface())
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot get last chat messages. Chat Interface is not valid."));
return false;
}
if (!UserId)
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot get last chat messages. User NetId is not valid."));
return false;
}
// Abort if not a private chat room.
if (RoomId.IsEmpty() || GetChatInterface()->GetChatRoomType(RoomId) != EAccelByteChatRoomType::PERSONAL)
{
UE_LOG_PRIVATECHAT(Warning, TEXT("Cannot get last chat messages. Room Id is empty or not a private chat room."));
return false;
}
GetChatInterface()->GetLastMessages(UserId.ToSharedRef().Get(), RoomId, NumMessages, OutMessages);
UE_LOG_PRIVATECHAT(Log, TEXT("Success to get last chat messages. Number of returned messages: %d"), OutMessages.Num());
return true;
}
Resources
-
The files used in this tutorial section are available in the Unreal Byte Wars GitHub repository.