Skip to main content

Implement Subsystem - Session chat - (Unreal Engine module)

Last updated on September 4, 2025

Unwrap the Subsystem

In this tutorial, you will learn how to implement session chat using the AGS Online Subsystem (OSS). In the Byte Wars project, a game instance subsystem named SessionChatSubsystem has already been created. This subsystem contains functions related to session chat. In this tutorial, you will use a starter version of that subsystem to implement session chat from scratch.

What's in the Starter Pack

To follow this tutorial, a starter subsystem class named SessionChatSubsystem_Starter has been prepared for you. This class consists of the following files:

  • Header file: /Source/AccelByteWars/TutorialModules/Social/SessionChat/SessionChatSubsystem_Starter.h
  • CPP file: /Source/AccelByteWars/TutorialModules/Social/SessionChat/SessionChatSubsystem_Starter.cpp

The SessionChatSubsystem_Starter class provides several functions, including:

  • An AGS OSS chat interface that you can use to implement session chat-related functions:

    FOnlineChatAccelBytePtr USessionChatSubsystem_Starter::GetChatInterface() const
    {
    const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
    if (!ensure(Subsystem))
    {
    UE_LOG_SESSIONCHAT(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());
    }
    FOnlineSessionV2AccelBytePtr USessionChatSubsystem_Starter::GetSessionInterface() const
    {
    const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
    if (!ensure(Subsystem))
    {
    UE_LOG_SESSIONCHAT(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<FOnlineSessionV2AccelByte>(Subsystem->GetSessionInterface());
    }
    FOnlineIdentityAccelBytePtr USessionChatSubsystem_Starter::GetIdentityInterface() const
    {
    const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
    if (!ensure(Subsystem))
    {
    UE_LOG_SESSIONCHAT(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 session chat messages.

    UPromptSubsystem* USessionChatSubsystem_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 USessionChatSubsystem_Starter::PushChatRoomMessageReceivedNotification(const FUniqueNetId& Sender, const FChatRoomId& RoomId, const TSharedRef<FChatMessage>& Message)
    {
    if (!GetChatInterface())
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot push chat room message received notification. Chat Interface is not valid."));
    return;
    }

    if (!GetPromptSubsystem())
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot push chat room message received notification. Prompt Subsystem is not valid."));
    return;
    }

    const FUniqueNetIdAccelByteUserRef SenderABId = StaticCastSharedRef<const FUniqueNetIdAccelByteUser>(Message->GetUserId());
    if (!SenderABId.Get().IsValid())
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot push chat room 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 type.
    const EAccelByteChatRoomType ChatRoomType = GetChatInterface()->GetChatRoomType(RoomId);
    const UCommonActivatableWidget* ActiveWidget = UAccelByteWarsBaseUI::GetActiveWidgetOfStack(EBaseUIStackType::Menu, this);
    if (const USessionChatWidget_Starter* SessionWidget = Cast<USessionChatWidget_Starter>(ActiveWidget))
    {
    if (SessionWidget->GetCurrentChatType() == ChatRoomType)
    {
    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());
    }

    switch (ChatRoomType)
    {
    case EAccelByteChatRoomType::SESSION_V2:
    // Push the notification only if the player already travelled to online session.
    if (GetWorld() && GetWorld()->GetNetMode() != ENetMode::NM_Standalone)
    {
    GetPromptSubsystem()->PushNotification(FText::Format(GAMESESSION_CHAT_RECEIVED_MESSAGE, FText::FromString(SenderStr)));
    }
    break;
    case EAccelByteChatRoomType::PARTY_V2:
    GetPromptSubsystem()->PushNotification(FText::Format(PARTY_CHAT_RECEIVED_MESSAGE, FText::FromString(SenderStr)));
    break;
    default:
    break;
    }

    // The push notification is not shown to the gameplay level. Instead, the game will show exclamation mark on the chat button.
    if (const FTutorialModuleGeneratedWidget* SessionChatGameplayButtonMetadata = FTutorialModuleGeneratedWidget::GetMetadataById(TEXT("btn_session_chat_gameplay")))
    {
    if (UAccelByteWarsButtonBase* Button = Cast<UAccelByteWarsButtonBase>(SessionChatGameplayButtonMetadata->GenerateWidgetRef))
    {
    Button->ToggleExclamationMark(true);
    }
    }
    }

Implement connect chat

In this section, you will implement functions to connect and reconnect the chat if it gets disconnected.

  1. 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
  2. Open the SessionChatSubsystem_Starter class header file and declare the following variable to store the number of reconnect retries:

    private:
    // ...
    int32 ReconnectChatNumTries = 0;
    const int32 ReconnectChatMaxTries = 3;
  3. Next, declare a new function to perform the chat reconnection.

    protected:
    // ...
    void ReconnectChat(FString Message);
  4. Open the SessionChatSubsystem_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 USessionChatSubsystem_Starter::ReconnectChat(FString Message)
    {
    if (!GetWorld() || GetWorld()->bIsTearingDown)
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Failed to reconnect to chat service. The level world is tearing down."));
    return;
    }

    if (!GetChatInterface())
    {
    UE_LOG_SESSIONCHAT(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_SESSIONCHAT(Warning, TEXT("Failed to reconnect to chat service. The player is not logged in yet."));
    return;
    }

    if (ReconnectChatNumTries >= ReconnectChatMaxTries)
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Failed to reconnect to chat service. Number tries to reconnect chat reached %d times limit."), ReconnectChatMaxTries);
    return;
    }

    UE_LOG_SESSIONCHAT(Log, TEXT("Try to reconnect chat due to: %s"), *Message);

    GetChatInterface()->Connect(0);
    ReconnectChatNumTries++;
    }
  5. Then, you need to bind the function above so that it will be called when the session chat message is disconnected. You can do this by adding the highlighted code below to the Initialize() function.

    void USessionChatSubsystem_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_SESSIONCHAT(Log, TEXT("Success to connect to chat service."));
    ReconnectChatNumTries = 0;
    return;
    }

    ReconnectChat(ErrorMessage);
    });
    GetChatInterface()->OnChatDisconnectedDelegates.AddUObject(this, &ThisClass::ReconnectChat);
    // ...
    }
    }
  6. 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 USessionChatSubsystem_Starter::Deinitialize()
    {
    // ...
    if (GetChatInterface())
    {
    // ...
    GetChatInterface()->OnConnectChatCompleteDelegates->RemoveAll(this);
    GetChatInterface()->OnChatDisconnectedDelegates.RemoveAll(this);
    // ...
    }
    }

Implement session chat utilities

In this section, you will implement some utilities for the session chat.

  1. Open the SessionChatSubsystem_Starter class header file and declare the following functions:

    public:
    // ...
    EAccelByteChatRoomType GetChatRoomType(const FString& RoomId);
    FString GetChatRoomIdBasedOnType(const EAccelByteChatRoomType ChatRoomType);
    FString GetGameSessionChatRoomId();
    FString GetPartyChatRoomId();
    bool IsMessageFromLocalUser(const FUniqueNetIdPtr UserId, const FChatMessage& Message);
  2. Next, open the SessionChatSubsystem_Starter class CPP file and define GetGameSessionChatRoomId(). This function will return the current active game session chat room ID.

    FString USessionChatSubsystem_Starter::GetGameSessionChatRoomId()
    {
    if (!GetChatInterface())
    {
    return FString();
    }

    if (!GetSessionInterface())
    {
    return FString();
    }

    const FNamedOnlineSession* GameSession = GetSessionInterface()->GetNamedSession(NAME_GameSession);
    if (!GameSession)
    {
    return FString();
    }

    return GetChatInterface()->SessionV2IdToChatTopicId(GameSession->GetSessionIdStr());
    }
  3. Still in the same file, define the GetPartyChatRoomId() to return the current active party session chat room ID.

    FString USessionChatSubsystem_Starter::GetPartyChatRoomId()
    {
    if (!GetChatInterface())
    {
    return FString();
    }

    if (!GetSessionInterface())
    {
    return FString();
    }

    const FNamedOnlineSession* PartySession = GetSessionInterface()->GetPartySession();
    if (!PartySession)
    {
    return FString();
    }

    return GetChatInterface()->PartyV2IdToChatTopicId(PartySession->GetSessionIdStr());
    }
  4. Next, define the GetChatRoomIdBasedOnType() to return chat room ID based on the chat room type. This is useful to encapstulate the functionality of GetGameSessionChatRoomId() and GetPartyChatRoomId().

    FString USessionChatSubsystem_Starter::GetChatRoomIdBasedOnType(const EAccelByteChatRoomType ChatRoomType)
    {
    FString ChatRoomId;

    switch (ChatRoomType)
    {
    case EAccelByteChatRoomType::SESSION_V2:
    ChatRoomId = GetGameSessionChatRoomId();
    break;
    case EAccelByteChatRoomType::PARTY_V2:
    ChatRoomId = GetPartyChatRoomId();
    break;
    }

    return ChatRoomId;
    }
  5. Then, define the GetChatRoomType() function to parse the chat room ID into chat room type.

    EAccelByteChatRoomType USessionChatSubsystem_Starter::GetChatRoomType(const FString& RoomId)
    {
    if (!GetChatInterface())
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot get chat room type for Room Id: %s"), *RoomId);

    return EAccelByteChatRoomType::NORMAL;
    }

    return GetChatInterface()->GetChatRoomType(RoomId);
    }
  6. Finally, 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 USessionChatSubsystem_Starter::IsMessageFromLocalUser(const FUniqueNetIdPtr UserId, const FChatMessage& Message)
    {
    if (!GetChatInterface())
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot check whether chat message is from local user or not. Chat Interface is not valid."));
    return false;
    }

    if (!UserId)
    {
    UE_LOG_SESSIONCHAT(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 session chat messages

In this section, you will implement functions to send a session chat message.

  1. Open the SessionChatSubsystem_Starter class header file and declare the following function:

    public:
    // ...
    void SendChatMessage(const FUniqueNetIdPtr UserId, const FChatRoomId& RoomId, const FString& Message);
  2. Next, declare a delegate and a callback function to handle when a session chat message is sent. You will use this delegate to connect with the session chat UI later.

    public:
    // ...
    FOnSendChatComplete* GetOnSendChatCompleteDelegates()
    {
    return &OnSendChatCompleteDelegates;
    }
    protected:
    void OnSendChatComplete(FString UserId, FString MsgBody, FString RoomId, bool bWasSuccessful);
    private:
    FOnSendChatComplete OnSendChatCompleteDelegates;
  3. Now, define the functions above. Open the SessionChatSubsystem_Starter class CPP file and define the SendChatMessage() function first to send a session chat message.

    void USessionChatSubsystem_Starter::SendChatMessage(const FUniqueNetIdPtr UserId, const FChatRoomId& RoomId, const FString& Message)
    {
    if (!GetChatInterface())
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot send chat message. Chat Interface is not valid."));
    OnSendChatComplete(FString(), Message, RoomId, false);
    return;
    }

    if (!UserId)
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot send chat message. User NetId is not valid."));
    OnSendChatComplete(FString(), Message, RoomId, false);
    return;
    }

    if (RoomId.IsEmpty())
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot send chat message. Room Id is empty."));
    OnSendChatComplete(FString(), Message, RoomId, false);
    return;
    }

    GetChatInterface()->SendRoomChat(UserId.ToSharedRef().Get(), RoomId, Message);
    }
  4. Next, define the OnSendChatComplete() function. When the send session chat request is complete, it will broadcast the delegate you created earlier to inform you that the process has completed.

    void USessionChatSubsystem_Starter::OnSendChatComplete(FString UserId, FString MsgBody, FString RoomId, bool bWasSuccessful)
    {
    if (bWasSuccessful)
    {
    UE_LOG_SESSIONCHAT(Log, TEXT("Success to send chat message on Room %s"), *RoomId);
    }
    else
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Failed to send chat message on Room %s"), *RoomId);
    }

    OnSendChatCompleteDelegates.Broadcast(UserId, MsgBody, RoomId, bWasSuccessful);
    }
  5. Then, you need to bind the callback function above so it will be called when the session 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 USessionChatSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    if (GetChatInterface())
    {
    GetChatInterface()->OnSendChatCompleteDelegates.AddUObject(this, &ThisClass::OnSendChatComplete);
    // ...
    }
    }
  6. 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 USessionChatSubsystem_Starter::Deinitialize()
    {
    // ...
    if (GetChatInterface())
    {
    GetChatInterface()->OnSendChatCompleteDelegates.RemoveAll(this);
    // ...
    }
    }

Implement receiving session chat messages

In this section, you will implement functions to handle when a session chat is received.

  1. Open the SessionChatSubsystem_Starter class header file and declare a delegate and a callback function to handle when a session 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 session chat UI later to show the received message.

    public:
    // ...
    FOnChatRoomMessageReceived* GetOnChatRoomMessageReceivedDelegates()
    {
    return &OnChatRoomMessageReceivedDelegates;
    }
    protected:
    // ...
    void OnChatRoomMessageReceived(const FUniqueNetId& UserId, const FChatRoomId& RoomId, const TSharedRef<FChatMessage>& Message);
    private:
    // ...
    FOnChatRoomMessageReceived OnChatRoomMessageReceivedDelegates;
  2. Next, open the SessionChatSubsystem_Starter class CPP file and define the OnChatRoomMessageReceived() function. When the session chat is received, it will broadcast the delegate you created earlier.

    void USessionChatSubsystem_Starter::OnChatRoomMessageReceived(const FUniqueNetId& UserId, const FChatRoomId& RoomId, const TSharedRef<FChatMessage>& Message)
    {
    UE_LOG_SESSIONCHAT(Log,
    TEXT("Received chat message from %s on Room %s: %s"),
    !Message.Get().GetNickname().IsEmpty() ? *Message.Get().GetNickname() : *UTutorialModuleOnlineUtility::GetUserDefaultDisplayName(Message->GetUserId().Get()),
    *RoomId,
    *Message.Get().GetBody());

    OnChatRoomMessageReceivedDelegates.Broadcast(UserId, RoomId, Message);
    }
  3. Then, you need to bind the callback function above so it will be called when the session 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 named PushChatRoomMessageReceivedNotification() to show a notification when a new chat is received.

    void USessionChatSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    if (GetChatInterface())
    {
    // ...
    GetChatInterface()->OnChatRoomMessageReceivedDelegates.AddUObject(this, &ThisClass::OnChatRoomMessageReceived);

    // Push a notification when received chat messages.
    GetChatInterface()->OnChatRoomMessageReceivedDelegates.AddUObject(this, &ThisClass::PushChatRoomMessageReceivedNotification);
    // ...
    }
    }
  4. 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 USessionChatSubsystem_Starter::Deinitialize()
    {
    // ...
    if (GetChatInterface())
    {
    // ...
    GetChatInterface()->OnChatRoomMessageReceivedDelegates.RemoveAll(this);
    // ...
    }
    }

Implement getting last session chat messages

In this section, you will implement functions to get the last session chat messages.

  1. Open the SessionChatSubsystem_Starter class header file and declare the following function:

    public:
    // ...
    bool GetLastChatMessages(const FUniqueNetIdPtr UserId, const FChatRoomId& RoomId, const int32 NumMessages, TArray<TSharedRef<FChatMessage>>& OutMessages);
  2. Next, open the SessionChatSubsystem_Starter class CPP file and define the function above. This function will return the last chat messages saved in the game cache.

    note

    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 USessionChatSubsystem_Starter::GetLastChatMessages(const FUniqueNetIdPtr UserId, const FChatRoomId& RoomId, const int32 NumMessages, TArray<TSharedRef<FChatMessage>>& OutMessages)
    {
    if (!GetChatInterface())
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot get last chat messages. Chat Interface is not valid."));
    return false;
    }

    if (!UserId)
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot get last chat messages. User NetId is not valid."));
    return false;
    }

    if (RoomId.IsEmpty())
    {
    UE_LOG_SESSIONCHAT(Warning, TEXT("Cannot get last chat messages. Room Id is empty."));
    return false;
    }

    GetChatInterface()->GetLastMessages(UserId.ToSharedRef().Get(), RoomId, NumMessages, OutMessages);
    UE_LOG_SESSIONCHAT(Log, TEXT("Success to get last chat messages. Returned messages: %d"), OutMessages.Num());

    return true;
    }

Resources