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("안녕하세요!"));
}