Unreal 게임 개발 과정 TIL

Today's I Learning

본캠프 TIL

09.15 Unreal 본캠프) 아웃라인 렌더링, 네임플레이트 회전축 버그, 세션 ListView 포커스, MVVM 슬라이더 동기화 & 디바운스 최적화 정리

Sunlight__ 2026. 9. 15. 20:34

📌 1. 들어가며 (Overview)

지난번 인게임 HUD와 일시정지 메뉴의 GC 크래시 및 시퀀서 버그를 수정한 이후, 플레이 경험을 완성하기 위한 오브젝트 상호작용 피드백, 세션 로비 UI 조작감 개선, 캐릭터 네임플레이트 위치 정렬, 그리고 설정창(Settings) 안정화 및 성능 최적화 작업을 연달아 진행했다.

단순한 기능 구현을 넘어, 렌더링 파이프라인(Custom Stencil), 스켈레탈 메시 트랜스폼 오프셋, Slate/UMG 입력 계층 구조, 그리고 MVVM 양방향 바인딩과 디스크 I/O 최적화까지 엔진 깊숙한 영역에서 발생했던 문제들을 분석하고 해결한 과정을 기록한다.

🛠️ 2. 핵심 트러블슈팅 & 기술 딥다이브

1) 대량의 화물(Cargo) 및 상호작용 오브젝트: Post Process Custom Stencil 아웃라인 시스템

🚨 문제 상황:
플레이어가 접근했을 때 상호작용 가능한 물건임을 알리기 위해 본체 머티리얼의 Emissive 값을 변경하는 방식을 시도했으나, 오브젝트 표면 텍스처 전체가 단색으로 덮여버려 시각적으로 부자연스러웠다. 또한 맵에 수많은 Cargo(화물)가 배치되어 있어 일일이 에셋을 수정하는 것은 유지보수 측면에서 불가능했다.

💡 해결 방안:

  • Post Process Material (Custom Stencil) 기반 외곽선 렌더링: 기존 프로젝트의 Depth/Stencil 샘플링 셰이더를 이식하여, 물체 표면은 유지한 채 실루엣 외곽선만 또렷하게 발광하도록 구현.
  • 동적 Stencil 토글: 대량의 액터에 불필요한 연산이 걸리지 않도록 플레이어가 상호작용 트리거/인터랙션 범위에 진입했을 때만 SetRenderCustomDepth(true) 및 SetCustomDepthStencilValue(1)를 런타임에 동적으로 켜고 끄도록 구조화.

2) 세션 로비 ListView: 마우스 우클릭으로만 선택되던 포커스/히트테스트 버그

🚨 문제 상황:
방 목록(Room List)에서 마우스 좌클릭으로 방을 선택하고 하단의 [Join] 버튼을 눌러야 하는데, 좌클릭으로는 아무런 반응이 없고 우클릭을 해야만 방이 선택되는 기현상이 발생.

💡 원인 및 해결 방안:

  • 원인: ListView 아이템 엔트리 위젯 내부의 텍스트나 보더 컨테이너의 Visibility 설정이 Visible로 되어 있어 좌클릭 이벤트를 엔트리 자식 위젯이 소비(Consume)해버렸고, 상위 ListView의 OnSelectionChanged 이벤트까지 도달하지 못함.
  • 해결: 마우스 입력을 직접 처리할 필요가 없는 텍스트/이미지 등 하위 데코레이션 요소들의 Visibility를 Self Hit Test Invisible로 명확히 분리하여, 부모 선택 영역이 마우스 좌클릭 이벤트를 정상 수신하도록 수정.

3) 캐릭터 네임플레이트: 주둥이 앞(얼굴 전방)으로 80cm 튀어나오던 축 오프셋 문제

🚨 문제 상황:
캐릭터 머리 위에 떠 있어야 할 닉네임 위젯(Nameplate)이 캐릭터 정수리가 아니라 얼굴 앞(주둥이 방향)으로 80cm 멀리 둥둥 떠 있는 현상 발생. Z축 오프셋을 올려도 머리 위로 가지 않고 대각선 앞쪽으로 뻗어나감.

💡 원인 및 해결 방안:

  • 원인: 네임플레이트 컴포넌트가 스켈레탈 메시의 head_socket 본에 부착되어 있었는데, 4족 보행/동물형 캐릭터 스켈레톤의 머리 본은 Pitch가 90도 회전되어 본의 로컬 Z축이 월드의 전방(X축)을 향하고 있었음.
  • 해결 (RootComponent 부착 + 동적 바운드 계산):
    1. 회전된 스켈레탈 본 대신 언제나 수직을 유지하는 GetRootComponent()(캡슐)에 네임플레이트를 부착.
    2. 정수리 기준 높이(머리 소켓의 월드 Z 높이와 캡슐 상단 비교)를 측정.
    3. 모자/헬멧 착용 시 대응: HatMeshComponent의 월드 바운드(Origin.Z + BoxExtent.Z)를 계산하여, 모자를 썼을 때도 모자 꼭대기 위 +25cm 지점에 FVector(0, 0, FinalZ)로 완벽하게 수직 중앙 정렬되도록 전면 개선.

4) 설정 메뉴 최적화: 슬라이더 드래그 시 매 프레임 디스크 Flush 병목 제거 (Debounced Save)

🚨 문제 상황:
마우스 감도나 볼륨 슬라이더를 드래그할 때 매 프레임(Tick)마다 SaveSettings()가 호출되어, 무거운 디스크 I/O인 GConfig->Flush()와 파일 시스템 해시 조회가 발생해 UI 드래그 중 프레임 드랍(버벅임)이 유발됨.

💡 해결: 메모리 즉시 반영 + 디바운스(0.3초) 디스크 플러시 분리

  • 조작 중에는 인메모리 GConfig와 로컬 플레이어 컨트롤러에 즉시 값을 전달하여 반응 딜레이 0ms 보장.
  • 무거운 하드디스크 쓰기(Flush)는 슬라이더 조작이 멈추고 0.3초 뒤에 1회만 일괄 실행되도록 타이머 기반 Debounced Save 패턴 구축.

5) FOV 설정: 텍스트 입력은 되는데 슬라이더만 80으로 고정되던 버그

🚨 문제 상황:
숫자를 타이핑하는 Input_FOV로는 FOV(시야각)가 잘 바뀌는데, 슬라이더(Slider_FOV)를 마우스로 아무리 드래그해도 값이 변하지 않고 작동하지 않음.

💡 원인 분석 및 해결:

  • 원인 1 (범위 불일치로 인한 Clamp 고정): UMG Slider의 기본 범위는 0.0 ~ 1.0인데, C++ ViewModel에서는 FMath::Clamp(NewValue, 80.0f, 100.0f)를 거치고 있었음. 슬라이더가 0.0~1.0을 넘겨주니 항상 최솟값인 80.0f로 고정되었던 것.
  • 원인 2 (바인딩 미등록 및 이중 바인딩 혼선): 바이너리 분석 결과 Slider_FOV는 EventGraph의 이벤트 노드도, MVVM 바인딩도 누락되어 있었음. 이후 EventGraph의 `OnValueChanged`와 MVVM Two-Way 바인딩을 동시에 추가하면 중복 호출(Double Binding)이 일어나므로, 깔끔하게 MVVM Two-Way 바인딩 하나로 단일화.
  • 원인 3 (에코 루프 방지): 슬라이더가 값을 바꾼 후 ViewModel이 다시 슬라이더로 브로드캐스트하여 발생하는 에코 루프를 막기 위해 bIsInternalUpdating 플래그 및 슬라이더 프로퍼티 역방송 차단 로직 적용.

💡 3. 핵심 요약 및 인사이트 (Takeaways)

  1. 스켈레탈 소켓 부착의 함정: 메시 본(Bone)은 모델링 제작자의 리깅 방식에 따라 축이 심하게 틀어져 있을 수 있다. 수직 위치 유지가 중요한 UI 컴포넌트는 본에 직접 붙이기보다 RootComponent(캡슐)를 기준으로 월드 높이를 동적 산출하는 것이 훨씬 견고하다.
  2. UMG Slider와 수치 매핑: UMG Slider는 기본값이 0~1이므로, C++에서 정수/실수 범위를 다룰 때는 Details 패널의 MinValue / MaxValue / StepSize를 C++ 허용 범위와 반드시 1:1로 일치시켜야 Clamp 고정 현상을 방지할 수 있다.
  3. MVVM vs EventGraph 이중 연결 주의: MVVM Two-Way 바인딩을 사용할 경우 블루프린트 EventGraph에서 `OnValueChanged` 이벤트를 따로 받아서 변수를 Set하면 Setter가 2회 연속 호출되는 부작용이 생긴다. 둘 중 하나로 창구를 일원화해야 한다.
  4. 실시간 슬라이더 조작 시 I/O 디바운싱: 설정창처럼 연속적인 슬라이더 조작이 일어나는 UI에서는 디스크 쓰기(Flush)를 절대 매 틱마다 부르면 안 되며, 메모리 즉시 반영 + 지연 저장(Debounce) 설계를 기본 규칙으로 삼아야 한다.