Skip to main content

Chat System Overview

The PlayNANOO Chat System provides real-time WebSocket-based chat functionality. This document explains the overall structure and usage of the chat system.

System Architecture

The chat system consists of 3 core classes.

ClassRole
ChatManagerSingleton manager that manages chat server connection and API calls
ChatClientCore class that handles WebSocket connection and message transmission/reception
ChatModelsDefines data models and event listener interfaces
Required Implementation Guide

The above 3 classes are required implementation classes, and can be freely modified to fit your project structure.

Required Implementation Elements:

  • Implementation of all callback methods in the IChatListener interface
  • Server list API call for WebSocket connection (PUT /chat/v20211101/server)
  • HMAC-SHA256 hash authentication logic implementation (refer to ChatClient)

Module Dependencies (Build.cs):

PublicDependencyModuleNames.AddRange(new string[]
{
"WebSockets", // WebSocket connection
"OpenSSL" // HMAC-SHA256 hash
});

Operation Flow

1. Call ChatManager.Connect()

2. Server list API request (PUT /chat/v20211101/server)

3. After receiving server list, select random server

4. ChatClient performs WebSocket connection

5. On connection success, call IChatListener.OnConnected()

6. Channel subscription and message transmission/reception

Step 1: Listener Implementation

To receive chat events, you must implement the IChatListener interface.

// MyChatHandler.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ChatModels.h"
#include "MyChatHandler.generated.h"

UCLASS()
class YOURPROJECT_API AMyChatHandler : public AActor, public IChatListener
{
GENERATED_BODY()

public:
// IChatListener implementation
virtual void OnConnected() override
{
// Server connection success
UE_LOG(LogTemp, Log, TEXT("Connected to chat server."));
}

virtual void OnDisconnected() override
{
// Server connection closed
UE_LOG(LogTemp, Log, TEXT("Disconnected from chat server."));
}

virtual void OnError(const FString& Code, const FString& Message) override
{
// Error occurred
UE_LOG(LogTemp, Error, TEXT("Error: [%s] %s"), *Code, *Message);
}

virtual void OnChannels(const TArray<FChatChannelInfo>& Channels) override
{
// Channel list received
for (const auto& Ch : Channels)
{
UE_LOG(LogTemp, Log, TEXT("Channel: %s, Users: %d"), *Ch.Channel, Ch.Count);
}
}

virtual void OnSubscribed(const FChatUserInfo& User) override
{
// User entered channel
UE_LOG(LogTemp, Log, TEXT("%s has entered."), *User.VisitorName);
}

virtual void OnUnSubscribed(const FChatUserInfo& User) override
{
// User left channel
UE_LOG(LogTemp, Log, TEXT("%s has left."), *User.VisitorName);
}

virtual void OnPublicMessage(const FChatUserInfo& Sender, const FString& Message) override
{
// Public message received
UE_LOG(LogTemp, Log, TEXT("[%s]: %s"), *Sender.VisitorName, *Message);
}

virtual void OnPrivateMessage(const FChatUserInfo& Sender, const FString& Message) override
{
// Whisper received
UE_LOG(LogTemp, Log, TEXT("[Whisper][%s]: %s"), *Sender.VisitorName, *Message);
}

virtual void OnNotifyMessage(const FChatUserInfo& Sender, const FString& Message) override
{
// System notification received
UE_LOG(LogTemp, Log, TEXT("[System]: %s"), *Message);
}

virtual void OnPlayerOnline(const TArray<FChatPlayerInfo>& Players) override
{
// Player online status received
for (const auto& P : Players)
{
FString Status = (P.Online == TEXT("Y")) ? TEXT("Online") : TEXT("Offline");
UE_LOG(LogTemp, Log, TEXT("%s: %s"), *P.UserUniqueId, *Status);
}
}
};

Step 2: Connect to Chat Server

Call ChatManager::GetInstance().Connect() to connect to the chat server.

void AMyChatHandler::BeginPlay()
{
Super::BeginPlay();

// Pass object that implements IChatListener
UChatManager::GetInstance()->Connect(this);
}

When connection is successful, the OnConnected() callback is called.

Step 3: Subscribe to Channel

After connecting, subscribe to desired channels.

void AMyChatHandler::OnConnected()
{
// Subscribe to global channel
UChatManager::GetInstance()->Subscribe(TEXT("global"));

// Or subscribe to specific channel
UChatManager::GetInstance()->Subscribe(TEXT("room_123"));
}

Step 4: Send Messages

Send Public Message

Send message to all users in the channel.

void AMyChatHandler::SendChatMessage(const FString& Text)
{
UChatManager::GetInstance()->SendPublicMessage(TEXT("global"), Text);
}

Send Whisper

Send message to specific user only.

void AMyChatHandler::SendWhisper(const FString& TargetUserId, const FString& Text)
{
UChatManager::GetInstance()->SendPrivateMessage(TargetUserId, Text);
}

Step 5: Other Features

Get Channel List

UChatManager::GetInstance()->GetChannels();
// Result received via OnChannels() callback

Unsubscribe from Channel

UChatManager::GetInstance()->Unsubscribe(TEXT("global"));

Get Player Online Status

TArray<FString> UserIds = {TEXT("user_001"), TEXT("user_002"), TEXT("user_003")};
UChatManager::GetInstance()->GetPlayersOnline(UserIds);
// Result received via OnPlayerOnline() callback

Check Connection Status

if (UChatManager::GetInstance()->IsConnected())
{
UE_LOG(LogTemp, Log, TEXT("Connected to chat server."));
}

Complete Usage Example

// ChatExample.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ChatManager.h"
#include "ChatModels.h"
#include "ChatExample.generated.h"

UCLASS()
class YOURPROJECT_API AChatExample : public AActor, public IChatListener
{
GENERATED_BODY()

public:
virtual void BeginPlay() override
{
Super::BeginPlay();

// 1. Connect to chat server
UChatManager::GetInstance()->Connect(this);
}

// 2. Subscribe to channel on connection success
virtual void OnConnected() override
{
UE_LOG(LogTemp, Log, TEXT("Connection successful!"));
UChatManager::GetInstance()->Subscribe(TEXT("global"));
}

// 3. Channel entry notification
virtual void OnSubscribed(const FChatUserInfo& User) override
{
UE_LOG(LogTemp, Log, TEXT("%s has entered."), *User.VisitorName);
}

// 4. Message received
virtual void OnPublicMessage(const FChatUserInfo& Sender, const FString& Message) override
{
UE_LOG(LogTemp, Log, TEXT("[%s]: %s"), *Sender.VisitorName, *Message);
}

// 5. Send message (called from UI button)
UFUNCTION(BlueprintCallable, Category = "Chat")
void OnSendButtonClick()
{
UChatManager::GetInstance()->SendPublicMessage(TEXT("global"), TEXT("Hello!"));
}

// Other callback implementations
virtual void OnDisconnected() override { UE_LOG(LogTemp, Log, TEXT("Disconnected")); }
virtual void OnError(const FString& Code, const FString& Message) override
{
UE_LOG(LogTemp, Error, TEXT("[%s] %s"), *Code, *Message);
}
virtual void OnChannels(const TArray<FChatChannelInfo>& Channels) override {}
virtual void OnUnSubscribed(const FChatUserInfo& User) override
{
UE_LOG(LogTemp, Log, TEXT("%s has left."), *User.VisitorName);
}
virtual void OnPrivateMessage(const FChatUserInfo& Sender, const FString& Message) override
{
UE_LOG(LogTemp, Log, TEXT("[Whisper] %s"), *Message);
}
virtual void OnNotifyMessage(const FChatUserInfo& Sender, const FString& Message) override
{
UE_LOG(LogTemp, Log, TEXT("[Notification] %s"), *Message);
}
virtual void OnPlayerOnline(const TArray<FChatPlayerInfo>& Players) override {}
};

Notes

  1. Service Call in Tick: ChatClient.Service() is called in ChatManager's Tick() to process WebSocket callbacks on the main thread. If you modify ChatManager directly, this must be maintained.

  2. Singleton Pattern: ChatManager is implemented as a singleton and accessed via UChatManager::GetInstance(). It persists across level transitions.

  3. Disconnect: The connection is automatically closed in ChatManager's destructor when the app exits.