2026년 10월 13일 영상 복습 으로 어제 작성하지 못한 코드를 작성을 하며 머리가 쪼게지기 시작했다.
채팅 UI는 입력창, 기록창, 스크롤 영역으로 나누었다.

C++에서는 다음과 같이 참조한다.
UPROPERTY(meta = (BindWidget))
TObjectPtr<UEditableTextBox> EditableTextBox_ChatInput;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UScrollBox> ChatScrollBox;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UMultiLineEditableTextBox> MultilineEditableTextBox_ChatLog;
BindWidget을 사용하므로 위젯 블루프린트의 요소 이름과 C++ 변수 이름을 맞춰야 한다.
TObjectPtr는 언리얼 객체를 참조하는 타입이다. 여기서는 UPROPERTY와 함께 선언해 위젯 객체를 참조한다.
메시지는 Enter를 눌렀을 때 보내도록 구성했다.
먼저 NativeConstruct에서 입력 확정 이벤트에 함수를 연결한다.
void UChatUI::NativeConstruct()
{
Super::NativeConstruct();
// 같은 함수가 이미 연결되어 있으면 다시 등록하지 않습니다.
if (!EditableTextBox_ChatInput->OnTextCommitted.IsAlreadyBound(
this, &UChatUI::OnChatInputTextCommitted))
{
EditableTextBox_ChatInput->OnTextCommitted.AddDynamic(
this, &UChatUI::OnChatInputTextCommitted);
}
}
OnTextCommitted는 입력이 확정됐을 때 알려주는 이벤트다.
AddDynamic으로 이벤트와 처리 함수를 연결하고, IsAlreadyBound로 중복 연결을 확인한다. 처리 함수에는 UFUNCTION()을 붙였다.
위젯이 해제될 때는 NativeDestruct에서 RemoveDynamic으로 연결을 제거한다.
EditableTextBox_ChatInput->OnTextCommitted.RemoveDynamic(
this, &UChatUI::OnChatInputTextCommitted);
이벤트를 연결하는 코드와 해제하는 코드를 함께 두어 위젯의 수명에 맞춰 관리했다.
입력 확정 이벤트에서는 확정된 이유를 CommitMethod로 전달받는다.
현재 코드는 다음 조건을 확인한다.
아래는 기존 처리 흐름을 읽기 쉽게 정리한 코드다.
void UChatUI::OnChatInputTextCommitted(
const FText& Text,
ETextCommit::Type CommitMethod)
{
// Enter 입력이며 내용이 있을 때만 처리합니다.
if (CommitMethod != ETextCommit::OnEnter || Text.IsEmpty())
{
return;
}
FString ChatMessage = Text.ToString();
// 다음 메시지를 입력할 수 있도록 입력창을 비웁니다.
EditableTextBox_ChatInput->SetText(FText::GetEmpty());
if (ACXPlayerController* PC = GetOwningPlayer<ACXPlayerController>())
{
if (PC->IsLocalController())
{
PC->CTSChatMessage(ChatMessage);
}
}
}
입력창에서는 FText로 내용을 받는다. 서버로 전달할 문자열은 ToString()으로 FString으로 변환한다.
현재 입력창을 비우는 시점은 서버의 수신 확인 후가 아니라 로컬에서 전송 처리를 시작하는 시점이다.
또한 IsEmpty()는 빈 문자열을 검사한다. 공백만 입력한 메시지를 막는 처리는 별도로 추가해야 한다.
ChatUI는 자신을 소유한 ACXPlayerController의 함수를 호출한다.
UFUNCTION(Server, Reliable)
void CTSChatMessage(const FString& Message);
CTSChatMessage는 서버에서 실행하도록 선언한 RPC다. 실제 본문은 _Implementation 함수에 작성한다.
void ACXPlayerController::CTSChatMessage_Implementation(
const FString& Message)
{
UE_LOG(LogTemp, Warning, TEXT("%s"), *Message);
for (TActorIterator<ACXPlayerController> It(GetWorld()); It; ++It)
{
if (ACXPlayerController* PC = *It)
{
PC->CTCChatMessage(
PlayerState->GetPlayerName(),
Message);
}
}
}
서버에서는 TActorIterator로 월드에 있는 ACXPlayerController를 순회한다.
여기서 이름을 읽는 PlayerState는 메시지를 보낸 컨트롤러의 상태다. 따라서 보내는 사람의 이름과 메시지를 함께 전달하려는 구조다.
TActorIterator는 플레이어를 별도 배열에 저장하는 자료구조가 아니라, 월드에서 해당 타입의 액터를 순회하는 도구다.
OnNewChatMessage에는 전달받은 메시지를 기존 기록 뒤에 붙이는 기능을 작성했다.
void UChatUI::OnNewChatMessage(
const FString& InSender,
const FString& InMessage)
{
FString NewMessage = InSender + TEXT(": ") + InMessage;
FString OldMessage =
MultilineEditableTextBox_ChatLog->GetText().ToString();
OldMessage += TEXT("\n");
MultilineEditableTextBox_ChatLog->SetText(
FText::FromString(OldMessage + NewMessage));
}
처리 순서는 다음과 같다.
보낸 사람과 메시지 조합
↓
기존 채팅 기록 읽기
↓
줄바꿈과 새 메시지 추가
↓
FText로 변환하여 기록창에 표시
예를 들어 다음과 같이 누적하는 방식이다.
Player1: 안녕하세요
Player2: 출발하겠습니다
다만 현재 코드는 기존 기록이 비어 있어도 줄바꿈을 먼저 추가한다. 첫 메시지 앞의 빈 줄을 없애려면 기존 기록이 있는 경우에만 줄바꿈을 넣도록 보완할 수 있다.
기존 차량 UI인 UCXUI 안에서 채팅 UI를 참조하도록 구성했다.
UPROPERTY(meta = (BindWidget))
TObjectPtr<UChatUI> UI_Chat;
PlayerController는 로컬 컨트롤러에서 전체 UI를 생성한다.
VehicleUI = CreateWidget<UCXUI>(this, VehicleUIClass);
VehicleUI->AddToViewport();
목표로 하는 메시지 전달 흐름은 다음과 같다.
ChatUI에서 Enter 입력
↓
CTSChatMessage: 서버로 전달
↓
서버가 플레이어 컨트롤러를 순회
↓
CTCChatMessage: 각 클라이언트로 전달
↓
VehicleUI → UI_Chat
↓
OnNewChatMessage로 기록 갱신
전체 UI는 채팅 UI로 메시지를 넘기고, 실제 문자열 표시 작업은 ChatUI가 담당하도록 나누는 구조다.
현재 코드에는 입력과 기록 표시 함수가 있지만, 서버에서 각 클라이언트의 채팅창까지 전달하는 연결은 아직 남아 있다.
현재 CTCChatMessage도 다음처럼 Server로 선언되어 있다.
UFUNCTION(Server, Reliable)
void CTCChatMessage(
const FString& Sender,
const FString& Message);
각 클라이언트에서 메시지를 받는 용도라면 다음 방향으로 수정해야 한다.
// 수정할 방향
UFUNCTION(Client, Reliable)
void CTCChatMessage(
const FString& Sender,
const FString& Message);
함수 이름에 어떤 약자를 붙였는지보다 UFUNCTION에 지정한 실행 위치가 중요하다.
현재 CTCChatMessage_Implementation은 로그만 출력한다.
앞으로 연결할 부분은 다음과 같다.
VehicleUI의 유효성을 확인한다.VehicleUI->OnNewChat(Sender, Message)를 호출한다.UCXUI::OnNewChat에서 UI_Chat->OnNewChatMessage로 전달한다.현재 UCXUI::OnNewChat은 헤더에 선언되어 있지만 cpp 구현은 없는 상태다.
ChatScrollBox도 참조는 준비되어 있으나, 메시지 추가 후 아래로 이동시키는 자동 스크롤 코드는 아직 없다.
PlayerState의 유효성을 확인해야 한다.BindWidget으로 블루프린트의 입력창과 기록창을 C++에 연결했다.OnTextCommitted에서 Enter 입력을 구분했다.FText와 FString을 변환해 입력과 표시를 처리했다.