하아찡

[언리얼5] 채팅 본문

C++/이것저것서버테스트

[언리얼5] 채팅

하아찡 2025. 2. 20. 18:09

언리얼 버전 5.5.3

서버언어 C++로 구성했습니다.


 

일단 채팅을 하기위한 채팅패킷 구조를 설계.

id -> 게임 내부 개인 ID값입니다. DB 아이디값이 아님.
msg -> 실제로 전달할 채팅 메세지
type -> 채팅 타입. 예를들어 전체, 파티, 궛말

 

위와같이 패킷 구조를 가지고있습니다.

id값이 DB에 저장하지않은 개인 ID값인 이유는 무엇인가?

서버에 접속한 유저에게 서버가 각각 고유 ID값을 1씩 증가시켜서 발급시켜주고, 해당 발급받은 ID를 가지고 Object를 구분을 하기때문에 서버단에서 처리할땐 따로발급해준 id값을 패킷으로 전달해준다.

즉, 해당 발급받은 ID는 서버도 가지고 있고 클라이언트도 자기 자신의 ID값을 가지고 있습니다.

 

msg는 말그대로 메세지 전달값입니다.

 

type은 현재는 구상만 해두고 작업이 들어가진 않았습니다. 나중에 파티시스템을 추가 했을때 작업이 들어갈 예정입니다.

 

 

그래서 위와같이 패킷 구조를 설계를 하여 데이터를 전달해줍니다.


 

이제는 클라이언트에서 패킷을 전달하기위해 UI작업을 들어갑니다.

UUserWidget을 상속받는 클래스를 생성해줍니다

 

저는 클래스명을 Chat_Widget으로 생성했습니다.

 

Chat_Widget.h

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/VerticalBox.h"
#include "Components/ScrollBox.h"
#include "Chat_Widget.generated.h"

UCLASS()
class LLLLLLLLLQQQ_API UChat_Widget : public UUserWidget
{
	GENERATED_BODY()

public:
    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UEditableTextBox* MyInputBox;  // 입력창

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    UVerticalBox* ChatLogBox;

    UPROPERTY(meta = (BindWidget))
    UScrollBox* ChatScrollBox;

    UFUNCTION(BlueprintCallable, Category = "SendMessge")
    void SendMsg();

    UFUNCTION(BlueprintCallable, Category = "GetMessge")
    void AddChatMessage(const int64 objectName,const FString& Message, int type);

};

 

 

Chat_Widget.cpp

#include "Chat_Widget.h"
#include "Components/TextBlock.h"
#include "Components/EditableTextBox.h"
#include "Components/VerticalBoxSlot.h"
#include "lllllllllqqq.h"
#include "Protocol.pb.h"
#include "MyGameInstance.h"
#include "lllllllllqqqCharacter.h"
#include "ServerPacketHandler.h"
#include "Kismet/GameplayStatics.h"


void UChat_Widget::SendMsg()
{
    if (MyInputBox)
    {
        FString Message = MyInputBox->GetText().ToString();
        if (!Message.TrimStartAndEnd().IsEmpty()) {

            APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 0);
            if (PlayerController)
            {
                AlllllllllqqqCharacter* MyCharacter = Cast<AlllllllllqqqCharacter>(PlayerController->GetPawn());
                if (MyCharacter)
                {

                    Protocol::C_CHAT pkt;

                    FTCHARToUTF8 Convert(*Message);
                    pkt.set_msg(Convert.Get());

                    SEND_PACKET(pkt);
                    AddChatMessage(MyCharacter->GetPlayerInfo()->object_id(), Message,0);
                    MyInputBox->SetText(FText::GetEmpty()); // 텍스트 초기화

                    MyCharacter->UpdateTextObjectID(Message);
                }
            }
        }

        
    }
}

void UChat_Widget::AddChatMessage(const int64 objectName, const FString& Message, int type)
{

    if (ChatLogBox)
    {
        // TextBlock 생성
        UTextBlock* NewChatMessage = NewObject<UTextBlock>(this, UTextBlock::StaticClass());
        if (NewChatMessage)
        {
            // 메시지 설정
            FString Msg = FString::Printf(TEXT("%d : %s"), objectName, *Message);
            NewChatMessage->SetText(FText::FromString(Msg));
            NewChatMessage->SetColorAndOpacity(FSlateColor(FLinearColor::White));

            // Vertical Box에 텍스트 추가
            UVerticalBoxSlot* NewSlot = ChatLogBox->AddChildToVerticalBox(NewChatMessage);
            if (NewSlot)
            {
                NewSlot->SetPadding(FMargin(5.0f));
                NewSlot->SetHorizontalAlignment(HAlign_Left);

                if (ChatScrollBox)
                {
                    // 살짝 딜레이를 줘야 정상적으로 스크롤 됨
                    FTimerHandle TimerHandle;
                    GetWorld()->GetTimerManager().SetTimer(TimerHandle, [this]()
                        {
                            ChatScrollBox->ScrollToEnd();
                        }, 0.01f, false);
                }
            }

        }
    }
}

 

SendMsg()는 메세지를 서버에 보낼때 사용합니다.

AddChatMessage()는 채팅창 Vertical Box에 TextBlock을 추가 해줍니다.


위에서 만든 클래스를 가지고 블루프린트를 생성합니다.

블루 프린트에서 ScrollBox와 VerticalBox 그리고 TextBox를 생성해줍니다.

그리고 각 컨트롤 마다 클래스에서 정해준 변수명과 동일하게 설정해줍니다.

저는 위와같은모양으로 생성했습니다.

 

 

그래프쪽으로 넘어가서

엔터를 눌렀을때 텍스트박스로 포커스가되게 해주고, 또 다시 눌렀을때는 메세지를 보내도록 함수를 호출합니다.

문자열이 있고없고는 SendMsg함수 내부에서 처리해주기때문에 그냥 함수 호출만 해줘도 될거같아서 위와같이 작업했습니다.


그러면 이제 해당 위젯을 등록해줘야하는데

저는 게임컨트롤러를 하나 생성해서 만들어줬습니다.

 

MyPlayerController.cpp

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "Blueprint/UserWidget.h"
#include "Chat_Widget.h"
#include "MyPlayerController.generated.h"

UCLASS()
class LLLLLLLLLQQQ_API AMyPlayerController : public APlayerController
{
	GENERATED_BODY()

public:
    virtual void BeginPlay() override;
    virtual void SetupInputComponent() override;

    // 채팅 위젯 클래스 (블루프린트와 연결)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI")
    TSubclassOf<UUserWidget> ChatWidgetClass;

    // 채팅 위젯 인스턴스
    UPROPERTY(BlueprintReadWrite)
    UChat_Widget* ChatWidgetInstance;

};

 

MyPlayerController.cpp

#include "MyPlayerController.h"
#include "Blueprint/UserWidget.h"
#include "Components/EditableTextBox.h"

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

    // 채팅 위젯 생성
    if (ChatWidgetClass)
    {
        ChatWidgetInstance = CreateWidget<UChat_Widget>(this, ChatWidgetClass);
        if (ChatWidgetInstance)
        {
            ChatWidgetInstance->AddToViewport();
            //ChatWidgetInstance->SetVisibility(ESlateVisibility::Hidden); // 처음엔 숨김
        }
    }
}

void AMyPlayerController::SetupInputComponent()
{
    Super::SetupInputComponent();

}

 

블루 프린트에서 채팅위젯을 등록해두고 해당 위젯을 BeginPlay함수에서 뷰포트에 추가해줍니다.

 

 

그리고 만든 위젯을 ChatWidgetClass에 등록해주시고 사용하시면됩니다.

 


서버측 코드

서버측 코드는 전문을 올리기에 많은 양이다보니 나중에 시간이 날때 올리겠습니다.

기본적으로 소켓으로 데이터를 받아서 처리하는 부분 로직을 설명하겠습니다.

 

bool Handle_C_CHAT(PacketSessionRef& session, Protocol::C_CHAT& pkt)
{
	GameSessionRef gameSession = static_pointer_cast<GameSession>(session);
	
	std::cout << "ObjectID : " << gameSession->player->playerInfo->object_id() << " Messge : " << pkt.msg() << endl;

	PlayerRef player = gameSession->player;

	Protocol::S_CHAT chatPkt;
	chatPkt.set_msg(pkt.msg());
	chatPkt.set_playerid(player->playerInfo->object_id());
	auto sendBuffer = ClientPacketHandler::MakeSendBuffer(chatPkt);

	// 보낸애 빼고 다 메세지 보냄
	GRoom->Broadcast(sendBuffer, player->playerInfo->object_id());

	return true;
}

 

위와같은 코드로 작업을 했습니다.

각 연결정보는 PacketSessionRef에게 담겨져있고 해당 PacketSession를 상속받은 GameSession으로 변경해서 각종 정보를 가져오게 됩니다.

여기에 연결된 플레이정보를 서버에서 할당받은 고유 ID값을 object_id에 저장해 두었기때문에 해당 ID값을 패킷에다가 같이 보내줘야합니다.

그래야 클라이언트가 받았을때 해당 ID값을 가지고 어떤 object인지 구분을 하게됩니다.

 


클라이언트 결과물

 


서버 결과물

 

반응형