Unreal 게임 개발 과정 TIL

Today's I Learning

본캠프 TIL

09.02 Unreal 본캠프) MVVM 기반 멀티플레이 인게임 HUD 시스템 구축기 & 트러블슈팅 2가지

Sunlight__ 2026. 9. 2. 19:36

📌 1. 들어가며 (Overview)

이번 프로젝트에서 멀티플레이 로비와 인게임 HUD, 그리고 3D 머리 위 닉네임 시스템 개발을 담당했다.

기존의 강결합 방식(위젯 내부에서 GetPlayerController나 GameMode를 직접 캐스팅하여 읽는 방식)을 지양하고, 언리얼 엔진 5의 공식 MVVM(Model-View-ViewModel) 플러그인을 적극 도입하여 UI와 게임 로직 간의 의존성을 완벽히 분리(Decoupling)하는 것을 핵심 목표로 잡았다.

 

🎮 이번에 구현한 핵심 기능

  • 1. 상단 게임 페이즈 배너:
    • 로비 상태: Press R to ReadyREADY! (Press R to Cancel) 실시간 키 입력 토글
    • 게임 진행: Waiting...Game Start!Mission Complete! 상태 자동 전환
  • 2. 우상단 화물 카운터:
    • GameState의 델리게이트 이벤트를 받아 남은 화물 개수 실시간 갱신 (Cargo: 12 / 20)
  • 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)

  1. 언리얼 MVVM 라이프사이클의 이해: 위젯의 메모리 생성(CreateWidget)과 뷰포트 마운트(AddToViewport) 시점의 Slate 트리 초기화 순서를 깊이 있게 이해할 수 있었다.
  2. 캡슐화와 방탄 아키텍처: 엔진 코어가 읽기 전용으로 보호하는 컴포넌트의 설계 의도를 파악하고, 선언적 바인딩과 명령형 함수 디스패치를 적재적소에 혼합하여 절대 깨지지 않는 UI 파이프라인을 구축했다.