跳转到主要内容

ChatManager

聊天系统的单例管理器类。管理聊天服务器连接及消息收发。

URL确认

此API使用 service-api.playnanoo.com 域名。

API信息

  • 服务器 列表 URL: https://service-api.playnanoo.com/chat/v20211101/server
  • 过滤URL: https://service-api.playnanoo.com/chat/v20211101/filter
  • Method: PUT
  • 需要认证: 否

主要功能

方法说明
Connect聊天 服务器 连接
IsConnected连接 状态 确认
Subscribe频道订阅 (上一 消息 数量 设置 可用)
Unsubscribe频道订阅 解除
GetChannels频道列表 查询
GetPlayersOnline玩家 在线 状态 查询
SendPublicMessage公开消息 发送
SendPrivateMessage发送私密消息
FetchFilterWords获取敏感词列表
Filter对消息应用敏感词过滤

Unreal C++实现

头文件 (ChatManager.h)

// ChatManager.h
#pragma once

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

UCLASS()
class YOURPROJECT_API AChatManager : public AActor
{
GENERATED_BODY()

public:
// 싱글톤 인스턴스
static AChatManager* GetInstance();

AChatManager();

virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

// Public API
void Connect(IChatListener* InListener);
bool IsConnected() const;
void Subscribe(const FString& Channel, int32 PrevMessageCount = 0);
void Unsubscribe(const FString& Channel);
void GetChannels();
void GetPlayersOnline(const TArray<FString>& UserIds);
void SendPublicMessage(const FString& Channel, const FString& Text);
void SendPrivateMessage(const FString& TargetUserId, const FString& Text);

// 금칙어 필터
void FetchFilterWords();
FString Filter(const FString& Message, TCHAR Separator = TEXT('*'));

private:
static const FString CHAT_SERVER_URL;
static const FString CHAT_FILTER_URL;

static AChatManager* Instance;

TUniquePtr<FChatClient> ChatClient;
IChatListener* Listener;
TArray<FString> FilterWords;

void FetchServerListAndConnect();
};

实现文件 (ChatManager.cpp)

// ChatManager.cpp
#include "ChatManager.h"
#include "PlayNANOOHelper.h"

const FString AChatManager::CHAT_SERVER_URL = TEXT("https://service-api.playnanoo.com/chat/v20211101/server");
const FString AChatManager::CHAT_FILTER_URL = TEXT("https://service-api.playnanoo.com/chat/v20211101/filter");

AChatManager* AChatManager::Instance = nullptr;

AChatManager* AChatManager::GetInstance()
{
return Instance;
}

AChatManager::AChatManager()
: Listener(nullptr)
{
PrimaryActorTick.bCanEverTick = true;
}

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

if (Instance != nullptr && Instance != this)
{
Destroy();
return;
}
Instance = this;
}

void AChatManager::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);

// Unreal WebSocket은 자동으로 이벤트를 처리하므로
// Unity처럼 Service() 호출이 필요하지 않습니다.
}

void AChatManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (Instance == this)
{
Instance = nullptr;
}

if (ChatClient.IsValid())
{
ChatClient->Disconnect();
ChatClient.Reset();
}

Super::EndPlay(EndPlayReason);
}

void AChatManager::Connect(IChatListener* InListener)
{
Listener = InListener;

// 사용자 닉네임 설정 (DataManager에서 가져온다고 가정)
FString Nickname = UGameDataManager::Get()->GetNickname();
ChatClient = MakeUnique<FChatClient>(Listener, Nickname);

FetchServerListAndConnect();
}

void AChatManager::FetchServerListAndConnect()
{
// 플레이어 정보가 포함된 요청 바디 생성
TSharedPtr<FJsonObject> Body = FPlayNANOOHelper::CreateRequestBody();
FString JsonBody = FPlayNANOOHelper::ToJsonString(Body);

// HTTP 요청
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->SetURL(CHAT_SERVER_URL);
Request->SetVerb(TEXT("PUT"));
FPlayNANOOHelper::SetCommonHeaders(Request, false); // 인증 토큰 불필요
Request->SetContentAsString(JsonBody);

Request->OnProcessRequestComplete().BindLambda(
[this](FHttpRequestPtr Req, FHttpResponsePtr Res, bool bSuccess)
{
if (bSuccess && Res.IsValid() && Res->GetResponseCode() >= 200 && Res->GetResponseCode() < 300)
{
FChatServerResponse ServerResponse;
if (ServerResponse.FromJson(Res->GetContentAsString()))
{
if (ServerResponse.Servers.Num() > 0)
{
// 랜덤 서버 선택
int32 Index = FMath::RandRange(0, ServerResponse.Servers.Num() - 1);
ChatClient->Connect(ServerResponse.Servers[Index]);
}
else if (Listener)
{
Listener->OnError(TEXT("SERVER_LIST_FAILED"), TEXT("No servers available"));
}
}
else if (Listener)
{
Listener->OnError(TEXT("SERVER_LIST_FAILED"), TEXT("Failed to parse server list"));
}
}
else if (Listener)
{
Listener->OnError(TEXT("SERVER_LIST_FAILED"), TEXT("HTTP request failed"));
}
});

Request->ProcessRequest();
}

bool AChatManager::IsConnected() const
{
return ChatClient.IsValid() && ChatClient->GetIsConnected();
}

void AChatManager::Subscribe(const FString& Channel, int32 PrevMessageCount)
{
if (ChatClient.IsValid())
{
ChatClient->Subscribe(Channel, PrevMessageCount);
}
}

void AChatManager::Unsubscribe(const FString& Channel)
{
if (ChatClient.IsValid())
{
ChatClient->Unsubscribe(Channel);
}
}

void AChatManager::GetChannels()
{
if (ChatClient.IsValid())
{
ChatClient->GetChannels();
}
}

void AChatManager::GetPlayersOnline(const TArray<FString>& UserIds)
{
if (ChatClient.IsValid())
{
ChatClient->GetPlayersOnline(UserIds);
}
}

void AChatManager::SendPublicMessage(const FString& Channel, const FString& Text)
{
if (ChatClient.IsValid())
{
ChatClient->SendPublicMessage(Channel, Text);
}
}

void AChatManager::SendPrivateMessage(const FString& TargetUserId, const FString& Text)
{
if (ChatClient.IsValid())
{
ChatClient->SendPrivateMessage(TargetUserId, Text);
}
}

void AChatManager::FetchFilterWords()
{
// 플레이어 정보가 포함된 요청 바디 생성
TSharedPtr<FJsonObject> Body = FPlayNANOOHelper::CreateRequestBody();
FString JsonBody = FPlayNANOOHelper::ToJsonString(Body);

// HTTP 요청
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->SetURL(CHAT_FILTER_URL);
Request->SetVerb(TEXT("PUT"));
FPlayNANOOHelper::SetCommonHeaders(Request, false); // 인증 토큰 불필요
Request->SetContentAsString(JsonBody);

Request->OnProcessRequestComplete().BindLambda(
[this](FHttpRequestPtr Req, FHttpResponsePtr Res, bool bSuccess)
{
if (bSuccess && Res.IsValid() && Res->GetResponseCode() >= 200 && Res->GetResponseCode() < 300)
{
FChatFilterResponse FilterResponse;
if (FilterResponse.FromJson(Res->GetContentAsString()))
{
FilterWords = FilterResponse.FilterWords;
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[Chat] Failed to fetch filter words"));
}
});

Request->ProcessRequest();
}

FString AChatManager::Filter(const FString& Message, TCHAR Separator)
{
if (FilterWords.Num() == 0)
{
return Message;
}

FString Result = Message;
for (const FString& Word : FilterWords)
{
if (!Word.IsEmpty())
{
FString Replacement;
for (int32 i = 0; i < Word.Len(); i++)
{
Replacement.AppendChar(Separator);
}
Result = Result.Replace(*Word, *Replacement, ESearchCase::IgnoreCase);
}
}
return Result;
}

使用示例

// ChatExample.h
#pragma once

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

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

public:
virtual void BeginPlay() override;

// IChatListener 구현
virtual void OnConnected() override;
virtual void OnDisconnected() override;
virtual void OnError(const FString& Code, const FString& Message) override;
virtual void OnChannels(const TArray<FChatChannelInfo>& Channels) override;
virtual void OnSubscribed(const FChatUserInfo& User) override;
virtual void OnUnSubscribed(const FChatUserInfo& User) override;
virtual void OnPublicMessage(const FChatUserInfo& Sender, const FString& Message) override;
virtual void OnPrivateMessage(const FChatUserInfo& Sender, const FString& Message) override;
virtual void OnNotifyMessage(const FChatUserInfo& Sender, const FString& Message) override;
virtual void OnPlayerOnline(const TArray<FChatPlayerInfo>& Players) override;

void SendMessage();
};

// ChatExample.cpp
#include "ChatExample.h"

void AChatExample::BeginPlay()
{
Super::BeginPlay();
AChatManager::GetInstance()->Connect(this);
}

void AChatExample::OnConnected()
{
UE_LOG(LogTemp, Log, TEXT("채팅 서버 연결 성공"));
// 금칙어 목록 가져오기
AChatManager::GetInstance()->FetchFilterWords();
// 이전 채팅 내역 10개 가져오기
AChatManager::GetInstance()->Subscribe(TEXT("global"), 10);
}

void AChatExample::OnDisconnected()
{
UE_LOG(LogTemp, Log, TEXT("채팅 서버 연결 해제"));
}

void AChatExample::OnError(const FString& Code, const FString& Message)
{
UE_LOG(LogTemp, Error, TEXT("채팅 오류: [%s] %s"), *Code, *Message);
}

void AChatExample::OnChannels(const TArray<FChatChannelInfo>& Channels)
{
for (const auto& Ch : Channels)
{
UE_LOG(LogTemp, Log, TEXT("채널: %s, 인원: %d"), *Ch.channel, Ch.count);
}
}

void AChatExample::OnSubscribed(const FChatUserInfo& User)
{
UE_LOG(LogTemp, Log, TEXT("%s님이 입장했습니다."), *User.visitorName);
}

void AChatExample::OnUnSubscribed(const FChatUserInfo& User)
{
UE_LOG(LogTemp, Log, TEXT("%s님이 퇴장했습니다."), *User.visitorName);
}

void AChatExample::OnPublicMessage(const FChatUserInfo& Sender, const FString& Message)
{
// 금칙어 필터 적용
FString FilteredMsg = AChatManager::GetInstance()->Filter(Message);
UE_LOG(LogTemp, Log, TEXT("[%s]: %s"), *Sender.visitorName, *FilteredMsg);
}

void AChatExample::OnPrivateMessage(const FChatUserInfo& Sender, const FString& Message)
{
// 금칙어 필터 적용
FString FilteredMsg = AChatManager::GetInstance()->Filter(Message);
UE_LOG(LogTemp, Log, TEXT("[귓속말][%s]: %s"), *Sender.visitorName, *FilteredMsg);
}

void AChatExample::OnNotifyMessage(const FChatUserInfo& Sender, const FString& Message)
{
FString FilteredMsg = AChatManager::GetInstance()->Filter(Message);
UE_LOG(LogTemp, Log, TEXT("[알림]: %s"), *FilteredMsg);
}

void AChatExample::OnPlayerOnline(const TArray<FChatPlayerInfo>& Players)
{
for (const auto& P : Players)
{
UE_LOG(LogTemp, Log, TEXT("플레이어: %s, 온라인: %s"), *P.userUniqueId, *P.online);
}
}

void AChatExample::SendMessage()
{
// 메시지 전송 (서버에 원본 그대로 전송)
AChatManager::GetInstance()->SendPublicMessage(TEXT("global"), TEXT("안녕하세요!"));
}