📌 1. 들어가며 (Overview)
이번 프로젝트에서 멀티플레이 로비와 인게임 HUD, 그리고 3D 머리 위 닉네임 시스템 개발을 담당했다.
기존의 강결합 방식(위젯 내부에서 GetPlayerController나 GameMode를 직접 캐스팅하여 읽는 방식)을 지양하고, 언리얼 엔진 5의 공식 MVVM(Model-View-ViewModel) 플러그인을 적극 도입하여 UI와 게임 로직 간의 의존성을 완벽히 분리(Decoupling)하는 것을 핵심 목표로 잡았다.
🎮 이번에 구현한 핵심 기능
- 1. 상단 게임 페이즈 배너:
- 로비 상태:
Press R to Ready↔READY! (Press R to Cancel)실시간 키 입력 토글 - 게임 진행:
Waiting...→Game Start!→Mission Complete!상태 자동 전환
- 로비 상태:
- 2. 우상단 화물 카운터:
- GameState의 델리게이트 이벤트를 받아 남은 화물 개수 실시간 갱신 (
Cargo: 12 / 20)
- GameState의 델리게이트 이벤트를 받아 남은 화물 개수 실시간 갱신 (
- 3. 좌하단 보이스 마이크 인디케이터:
V키 입력을 통한 마이크 음소거/해제 실시간 Widget Switcher 전환
- 4. 3D 머리 위 네임플레이트:
- Screen-Space 3D 빌보드 위젯 컴포넌트 부착 (내 캐릭터는 숨김, 타 플레이어 머리 위에만 닉네임 렌더링)
🔥 2. 핵심 트러블슈팅 2선 (Troubleshooting)
🔴 Issue 1: UMG 라이프사이클 미숙지로 인한 LoadedProperties 0 on 0 에러
[문제 상황]
C++ PlayerController의 BeginPlay()에서 HUD 위젯을 생성하고 ViewModel을 주입하자마자 에디터 Output Log에 아래와 같은 빨간 에러가 발생하며 바인딩이 런타임에 중단되었다.
LogMVVM: Error: LoadedProperties.IsValidIndex(PathIndex.Index) failed. '0 on 0'
Model View Viewmodel: Error: The binding was not executed. Invalid Source.
[원인 분석]
언리얼 엔진 5의 MVVM 시스템(UMVVMView)은 위젯이 생성된 직후가 아니라, AddToViewport()를 통해 화면(Slate 트리)에 실제로 마운트될 때 비로소 위젯 에셋의 바인딩 속성 목록(LoadedProperties)을 메모리에 로드한다.
기존 코드는 AddToViewport()를 호출하기 전에 SetViewModel()과 ExecuteViewModelBindings()를 먼저 실행했기 때문에, 속성 목록이 비어 있는(0 on 0) 상태에서 바인딩이 평가되어 에러가 발생했던 것이다.
[해결 방법]
위젯을 먼저 슬레이트 뷰포트에 추가하여 LoadedProperties를 온전히 활성화한 뒤 ViewModel을 주입하도록 호출 순서를 교정했다.
// 1. 위젯을 먼저 뷰포트에 등록 (LoadedProperties 활성화)
HUDWidget->AddToViewport(0);
// 2. 그 다음 안전하게 ViewModel 주입 및 바인딩 평가
if (UMVVMView* View = UMVVMSubsystem::GetViewFromUserWidget(HUDWidget))
{
View->SetViewModel(FName("Ch4HUDViewModel"), HUDViewModel);
View->ExecuteViewModelBindings(FName("Ch4HUDViewModel"));
}
🔴 Issue 2: UWidgetSwitcher의 BlueprintReadOnly 캡슐화와 다이렉트 디스패치
[문제 상황]
V키 마이크 토글 시 마이크 ON/OFF 아이콘을 교체하기 위해 Widget Switcher의 Active Widget Index를 MVVM 바인딩 창에 연결하려 했으나, 노란색 경고 삼각 표지판(⚠️)이 뜨며 컴파일 에러 또는 런타임 무시 현상이 발생했다.
[원인 분석]
Widget Switcher의 ActiveWidgetIndex 변수는 에픽게임즈 엔진 코어에서 BlueprintReadOnly(읽기 전용)로 엄격하게 잠겨 있다.
위젯 스위처는 단순히 숫자만 바꾼다고 끝나는 것이 아니라, "이전 자식 위젯을 Collapsed로 숨기고, 새 자식 위젯을 Visible로 켜고, Slate 레이아웃 크기를 다시 계산하는 내부 작업"이 반드시 동반되어야 한다. 그래서 변수에 직접 쓰는 것을 막고 반드시 SetActiveWidgetIndex() 함수를 통해서만 바꾸도록 설계되어 있었던 것이다.
[해결 방법]
MVVM 변수 바인딩 대신, 위젯에 SetMicActive 함수를 만들어 두고 PlayerController에서 V키가 눌릴 때 C++ ProcessEvent로 위젯 함수를 즉시 직접 호출하는 다이렉트 디스패치(Direct Dispatch) 방식으로 우회하여 0.000초 즉시 반응하는 완벽한 방탄 처리를 완성했다.
void ACh4_multiGamePlayerController::ToggleVoice()
{
if (!IsLocalPlayerController()) return;
// 1. ViewModel 데이터 갱신
if (HUDViewModel)
{
HUDViewModel->ToggleMic();
}
// 2. WBP_HUD의 SetMicActive 함수를 즉시 직접 호출 (방탄 처리)
if (HUDWidget)
{
if (UFunction* SetMicFunc = HUDWidget->FindFunction(FName("SetMicActive")))
{
struct FSetMicParams { bool bIsActive; };
FSetMicParams Params;
Params.bIsActive = HUDViewModel ? HUDViewModel->bIsMicActive : false;
HUDWidget->ProcessEvent(SetMicFunc, &Params);
}
}
}
🎯 3. 마치며 & 배운 점 (Takeaways)
- 언리얼 MVVM 라이프사이클의 이해: 위젯의 메모리 생성(
CreateWidget)과 뷰포트 마운트(AddToViewport) 시점의 Slate 트리 초기화 순서를 깊이 있게 이해할 수 있었다. - 캡슐화와 방탄 아키텍처: 엔진 코어가 읽기 전용으로 보호하는 컴포넌트의 설계 의도를 파악하고, 선언적 바인딩과 명령형 함수 디스패치를 적재적소에 혼합하여 절대 깨지지 않는 UI 파이프라인을 구축했다.
'본캠프 TIL' 카테고리의 다른 글
| 09.15 Unreal 본캠프) 아웃라인 렌더링, 네임플레이트 회전축 버그, 세션 ListView 포커스, MVVM 슬라이더 동기화 & 디바운스 최적화 정리 (0) | 2026.09.15 |
|---|---|
| 09.07 Unreal 본캠프) MVVM 피팅룸 스킨·액세서리 연동 및 피직스 래그돌 크래시 해결기 (0) | 2026.09.07 |
| 08.25 Unreal 본캠프) 멀티플레이어 일시정지(Pause) 한계와 MVVM 기반 메뉴 구현 (0) | 2026.08.25 |
| 08.19 Unreal 본캠프) 메인 메뉴 UI 구현과 세션 네트워크(LAN/Steam) 예외 처리 (0) | 2026.08.19 |
| 08.06 Unreal 본캠프) 숫자 야구 게임 버그 수정 및 입력 유효성 검사 구현 (0) | 2026.08.06 |