1. 개요 및 멀티플레이어 프레임워크 기초
1-1. 왜 이것이 중요한가
언리얼 엔진 멀티플레이어 개발에서 네트워크 프레임워크의 기초를 올바르게 구성하는 것은 팀 프로젝트의 성패를 가르는 핵심입니다. 멀티플레이어 게임은 모든 클라이언트가 동일한 규칙과 논리 흐름 안에서 움직여야 하므로, 서버 중심의 구조 설정을 처음부터 견고하게 다져야 합니다.
멀티플레이어 게임은 서버와 클라이언트가 명확히 분리된 상태에서 패킷을 주고받아야 하므로, 초기 환경 설정과 C++ 프레임워크 구조(GameMode, PlayerController)를 올바르게 세팅하지 않으면 이후 학습할 RPC 동기화나 변수 복제(Replication) 코드가 정상적으로 작동하지 않고 빌드 오류를 야기합니다. 이번 내용에서는 UE 5.5 C++ 환경에서 프로젝트를 생성하고, 채팅 시스템의 기반이 되는 사용자 정의 GameModeBase와 PlayerController를 연동하여 멀티플레이 프레임워크의 첫 단추를 채울 예정입니다.
2. 언리얼 엔진 멀티플레이어 프레임워크 핵심 액터
언리얼 엔진 멀티플레이어 아키텍처에서 가장 중요한 규칙 중 하나는 GameMode 액터는 오직 서버에만 존재한다는 점입니다. 반면, 각 클라이언트를 대변하는 PlayerController는 해당 클라이언트 본인과 서버에만 존재합니다. 이 구조를 정확히 이해해야 보안과 동기화 오류를 방지할 수 있습니다. 이를 인지하고 C++ 부모 클래스를 올바르게 상속받아 에디터의 World Settings에 연동해야 합니다.
- GameModeBase (ACXGameModeBase): 게임의 전체적인 규칙, 승리 조건, 스폰 로직을 통제하는 심판 역할을 합니다. 데디케이티드 서버 아키텍처에서는 오직 서버 인스턴스에만 스폰되며 클라이언트는 이 액터에 직접 접근할 수 없습니다.
- PlayerController (ACXPlayerController): 플레이어의 입력(키보드, 마우스, UI 이벤트)과 UI(인터페이스)를 처리하는 핵심 네트워크 통신 창구(네트워크 오너십의 시작점)입니다. 서버는 접속한 모든 플레이어의 PlayerController를 가지고 있지만, 클라이언트 A는 클라이언트 B의 PlayerController를 가질 수 없습니다.
3. UMG와 C++ 클래스 간의 meta = (BindWidget) 동적 바인딩

블루프린트로 UI를 디자인(WBP_ChatInput)하고, 실제 논리 코드는 C++(UCXChatInput)에서 제어하기 위해 meta = (BindWidget) 속성을 사용합니다. 이 방식을 사용하면 성능을 최적화하고 디자이너와 프로그래머의 협업 효율을 극대화할 수 있습니다.
// UI 컴포넌트를 C++ 포인터와 이름 일치로 자동 바인딩
UPROPERTY(meta = (BindWidget))
TObjectPtr<UEditableTextBox> EditableTextBox_ChatInput;
- 동작 규칙: 블루프린트 위젯 내의 컴포넌트 이름(EditableTextBox_ChatInput)과 C++ 변수명이 정확히 일치해야만 런타임에 크래시 없이 안전하게 인스턴스가 바인딩됩니다.
3-1. 바인딩 절차 및 구조
- Build.cs 모듈 추가 (UMG 관련 종속성 설정): C++에서 UMG 및 Slate UI 시스템 핵심 컴포넌트를 인식하고 제어할 수 있도록 ChatX.Build.cs 파일의 PublicDependencyModuleNames 영역에 "UMG", "Slate", "SlateCore" 모듈을 명시적으로 추가합니다.
- C++ 매크로 선언 (UPROPERTY 변수 매칭): C++ 헤더 파일에 위젯 컴포넌트 변수를 선언할 때, 블루프린트 위젯의 계층 구조 내 이름과 정확히 일치하는 이름으로 UPROPERTY(meta = (BindWidget))을 지정합니다.
- 델리게이트 동적 바인딩 (NativeConstruct 내 이벤트 연결): 위젯의 생명주기 메서드인 NativeConstruct()에서 OnTextCommitted.AddDynamic()을 호출하여 플레이어가 엔터 키를 눌렀을 때 실행될 C++ 함수를 연결하고, 라이프사이클에 따라 제거될 때(NativeDestruct) 바인딩을 해제합니다.
4. 로컬 UI 입력 이벤트 연동 및 로컬 출력 파이프라인
이론을 바탕으로 실제 작동하는 채팅 입력 및 출력 파이프라인을 구축해 보겠습니다. 이번 예제는 클라이언트가 입력한 텍스트를 내부 변수에 저장하고 화면에 출력하는 ordered procedure입니다.
Worked Example 1: UI 입력 이벤트의 C++ 연동 및 로컬 출력
4-1. 플레이어 컨트롤러 초기화 및 UI 모드 설정
게임이 시작되면 플레이어의 입력이 UI에만 집중되도록 FInputModeUIOnly를 설정하고, 생성된 위젯 인스턴스를 뷰포트에 추가합니다.
// CXPlayerController.cpp
void ACXPlayerController::BeginPlay() {
Super::BeginPlay();
FInputModeUIOnly InputModeUIOnly;
SetInputMode(InputModeUIOnly);
if (IsValid(ChatInputWidgetClass)) {
ChatInputWidgetInstance = CreateWidget(this, ChatInputWidgetClass);
if (IsValid(ChatInputWidgetInstance)) {
ChatInputWidgetInstance->AddToViewport();
}
}
}
4-2. 텍스트 커밋 이벤트 처리 및 컨트롤러 데이터 전달
플레이어가 엔터를 눌러 입력을 확정(ETextCommit::OnEnter)하면, OnChatInputTextCommitted가 실행됩니다. GetOwningPlayer()를 호출하여 나를 소유한 로컬 APlayerController를 찾고, 이를 우리가 만든 ACXPlayerController로 안정적으로 다운캐스팅(Cast)하여 전송한 뒤 입력창을 비웁니다.
// CXChatInput.cpp
void UCXChatInput::OnChatInputTextCommitted(const FText& Text, ETextCommit::Type CommitMethod) {
if (CommitMethod == ETextCommit::OnEnter) {
APlayerController* OwningPlayerController = GetOwningPlayer();
if (IsValid(OwningPlayerController)) {
ACXPlayerController* OwningCXPlayerController = Cast<ACXPlayerController>(OwningPlayerController);
if (IsValid(OwningCXPlayerController)) {
OwningCXPlayerController->SetChatMessageString(Text.ToString());
EditableTextBox_ChatInput->SetText(FText());
}
}
}
}
4-3. 디버그 문자열을 통한 데이터 출력 확인
캐스팅이 성공하여 컨트롤러로 넘어온 문자열 데이터를 수신하면, 내부 변수 ChatMessageString에 저장하고 UKismetSystemLibrary::PrintString을 사용하여 화면에 붉은색 텍스트로 채팅 데이터를 로컬 출력합니다.
// CXPlayerController.cpp
void ACXPlayerController::SetChatMessageString(const FString& InChatMessageString) {
ChatMessageString = InChatMessageString;
PrintChatMessageString(ChatMessageString);
}
void ACXPlayerController::PrintChatMessageString(const FString& InChatMessageString) {
UKismetSystemLibrary::PrintString(this, ChatMessageString, true, true, FLinearColor::Red, 5.0f);
}
5. 멀티플레이어 환경에서의 UI 안전 생성 및 PIE 독립 프로세스 설정
5-1. 왜 이것이 중요한가
멀티플레이어 UI 개발에서 가장 빈번하게 발생하는 오류 중 하나는 "내가 입력한 UI가 다른 플레이어의 화면이나 서버에 중복으로 생성되거나 출력되는 현상"입니다. 이는 단일 프로세스 메모리 공유 환경(PIE)의 특성과 액터의 생성 컨텍스트를 정확히 제어하지 못해 발생합니다. IsLocalController()를 활용한 제어권 필터링과 Run Under One Process 옵션을 해제하여 실제 네트워크와 동일한 독립 프로세스 환경을 구축하는 방법을 이해해야만, 팀 프로젝트에서 UI 패킷 누락이나 크래시가 없는 안정적인 프레임워크를 완성할 수 있습니다.
5-2. Core Concepts
5-2-1. IsLocalController()를 활용한 UI 생성 제어

언리얼 엔진의 멀티플레이어 아키텍처에서 APlayerController는 서버와 해당 소유 클라이언트에게만 존재합니다. 하지만 서버는 접속한 모든 클라이언트의 PlayerController 인스턴스를 가집니다. 만약 BeginPlay()에서 아무런 조건 없이 위젯을 생성하고 뷰포트에 추가한다면, 서버 권한(Authority) 단에서도 원격 클라이언트의 수만큼 위젯을 생성하려는 논리적 오작동이 발생합니다.
- IsLocalController(): 현재 이 PlayerController 코드가 실행되고 있는 호스트(클라이언트 또는 서버)가 이 컨트롤러를 직접 제어하는 로컬 유저인지 판별합니다.
- 방어적 코드 패턴: 원격 프록시에 의한 중복 위젯 생성을 방지하고 클라이언트 본인의 화면에만 UI 위젯을 띄우려면, 위젯 생성 코드 직전에 반드시 IsLocalController() 검증을 거쳐야 합니다.
5-2-2. PIE 멀티플레이어 프로세스 분리 아키텍처
에디터 개인설정(Editor Preferences)의 Net Play 스위치들은 테스트 환경의 물리적 구조를 결정합니다.
- Run Under One Process (체크 상태): 하나의 OS 프로세스 안에서 여러 클라이언트와 서버를 가상으로 구동합니다. 메모리를 공유하므로 C++의 UKismetSystemLibrary::PrintString 같은 화면 디버그 메시지가 프로세스 경계를 넘어 모든 가상 화면에 한꺼번에 도배되는 디버깅 오버헤드가 발생합니다.
- Run Under One Process (체크 해제): 서버와 각 클라이언트를 실제 Windows/Mac 독립 프로세스로 완전히 격리하여 구동합니다. 실제 라이브 서비스와 동일한 네트워크 환경 및 포트 통신을 시뮬레이션하므로 정밀한 멀티플레이 트러블슈팅이 가능해집니다.
5-3. PIE 독립 환경 세팅 절차
- PIE 설정 변경 (에디터 기본 환경 세팅): Editor Preferences → Level Editor → Play 에 진입하여 Play Net Mode를 Play as Client로, Number of Players를 2로 설정합니다.
- 프로세스 격리 (공유 메모리 차단): Play 세부 항목 중 Run Under One Process 옵션을 체크 해제(False)하여 가상 클라이언트들이 개별 프로세스로 스폰되도록 바꿉니다.
- Launch Separate Server 활성화 (데디케이티드 서버 격리): Launch Separate Server 옵션을 켜서 백그라운드에서 순수 Headless 데디케이티드 서버 프로세스가 독립적으로 수신 대기(Listen)하도록 유도합니다.
5-4. Worked Example 2: 로컬 컨트롤러 판별을 통한 UI 위젯 안전 생성 프로시저
앞서 구현한 ACXPlayerController 채팅 시스템 위에, 원격 프록시에 의한 중복 위젯 생성을 방지하고 안전하게 로컬 UI를 빌드하는 절차를 구현해 보겠습니다.
5-4-1. 로컬 제어 권한 검증
ACXPlayerController::BeginPlay() 내부에서 가장 먼저 로컬 플레이어가 제어하는 인스턴스인지 판별합니다.
void ACXPlayerController::BeginPlay()
{
Super::BeginPlay();
// 1. 방어적 코드: 로컬 컨트롤러가 아니라면 UI 생성 로직을 완전히 차단
if (IsLocalController() == false)
{
return;
}
// 2. 로컬 클라이언트인 경우에만 입력 모드를 UI 전용으로 전환
FInputModeUIOnly InputModeUIOnly;
SetInputMode(InputModeUIOnly);
// 3. 위젯 인스턴스 바인딩 및 출력 파이프라인 진행...
if (IsValid(ChatInputWidgetClass) == true)
{
ChatInputWidgetInstance = CreateWidget(this, ChatInputWidgetClass);
if (IsValid(ChatInputWidgetInstance) == true)
{
ChatInputWidgetInstance->AddToViewport();
}
}
}
'Unreal Engine > 멀티플레이어 게임 개발' 카테고리의 다른 글
| [Remote Procedure Call 기초와 멀티플레이 채팅 구현] Remote Procedure Call (1) | 2026.08.05 |
|---|---|
| [언리얼 멀티플레이 기초 개념] 네트워크 동기화 구현 (RPC) (1) | 2026.08.04 |
| [언리얼 멀티플레이 기초 개념] 언리얼 네트워크 심화 메커니즘 (1) | 2026.08.03 |
| [언리얼 멀티플레이 기초 개념] 멀티플레이어 기본 개념 및 네트워크 아키텍처 (2) | 2026.07.30 |