지난 글까지 방 흐름과 인게임 HUD 를 붙였다. 이번에는 환경설정 화면을 메인 메뉴와 인게임 양쪽에서 재사용하는 구조를 잡았다.
기능 자체는 크지 않은데, 기존 UI 구조에 없던 것 세 가지가 드러났다. 화면을 닫는 경로, 인게임 입력 모드 복원, HUD 레이어 제어다.
| 주제 | 문제 | 해결 |
|---|---|---|
| 해상도 · 그래픽 품질 | 직접 구현할 필요가 있나 | 언리얼 내장 지원 기능이라 C++ 0줄 |
| 화면 닫기 | 인게임에서 페이지를 닫을 경로가 없음 | ClosePage() 추가 |
| 입력 모드 | GameOnly 로 복원하면 커서가 사라짐 |
복원 모드를 파라미터로 받음 |
| 콤보박스 초기화 | 화면을 열기만 해도 설정이 바뀜 | 초기화 가드 플래그 |
| HUD | 설정 화면 위에 HUD 가 겹침 | 레이어 단위 표시 전환 |
해상도와 그래픽 품질 설정은 언리얼 내장 지원 기능이다. 별도 저장 시스템이나 적용 코드를 만들 필요가 없다.
Get Game User Settings
├ Set Screen Resolution
├ Set Fullscreen Mode
├ Set Overall Scalability Level
├ Apply Settings
└ Save Settings
전부 블루프린트 노드로 열려 있어서 C++ 을 한 줄도 안 쓰고 만들 수 있다. 저장도 Save Settings 한 번이면 Saved/Config/.../GameUserSettings.ini 에 기록되고 다음 실행 때 자동으로 복원된다.
그래픽 품질이 특히 값이 좋다. 스케일러빌리티는 그림자 해상도·뷰 디스턴스·텍스처 밉·이펙트·포스트프로세싱 같은 엔진 전역 설정이라 에셋마다 붙이는 게 아니다. 지금 넣어두면 앞으로 들어올 맵·캐릭터·도구에 전부 걸린다.
엔진 레벨은 다섯 단계다.
0 Low 1 Medium 2 High 3 Epic 4 Cinematic
콤보박스를 「낮음 / 중간 / 높음」 세 개로 만들면 인덱스와 레벨이 더 이상 같지 않다. 양방향 매핑이 필요하다.
적용 콤보 0 → 0 콤보 1 → 1 콤보 2 → 3 (Epic)
표시 0 → 콤보 0 1 → 콤보 1 2·3·4·-1 → 콤보 2
「높음」을 2(High) 가 아니라 3(Epic) 으로 보내는 이유가 있다. 프로젝트 기본값이 Epic 인데 「높음」을 2 로 매핑하면, 설정 화면을 한 번 여는 것만으로 화질이 조용히 내려간다. 아무것도 안 바꿨는데 게임이 달라 보이면 버그로 의심받는다.
-1 은 개별 그래픽 설정이 섞여 있을 때 반환되는 값이다. 그대로 Set Selected Index 에 넣으면 범위를 벗어난다.
지금까지 화면 전환은 전부 다른 화면으로 바꾸는 것이었다. Title → MainMenu → Room → Result 처럼.
그런데 인게임 ESC 메뉴는 다르다. 닫으면 화면이 없는 상태로 돌아가야 한다.
const TSubclassOf<UCWUIScreenWidget>* FoundClass = PageClasses.Find(TargetPage);
if (FoundClass == nullptr || !(*FoundClass))
{
return false;
}
OpenPage(None) 은 PageClasses 에 None 이 없어서 여기서 실패한다. 화면을 정리하는 CloseCurrentPage() 는 있지만 private 이라 위젯에서 부를 수 없다.
「화면 없음」이 목적지가 되는 경우를 처음부터 상정하지 않았던 것이다.
공개 함수를 하나 추가했다.
UFUNCTION(BlueprintCallable, Category = "CW|UI")
bool ClosePage(
ECWUIInputMode ReturnInputMode = ECWUIInputMode::GameAndUI
);
BeginTransition();
CloseCurrentPage();
CurrentPage = ECWUIPage::None;
ApplyInputMode(ReturnInputMode);
EndTransition();
메인 메뉴에서는 이 함수를 쓰면 안 된다. 메뉴 맵에는 돌아갈 화면이 없어서 아무것도 없는 화면이 남는다.
메인 메뉴에서 뒤로 RequestOpenPage(MainMenu)
인게임에서 뒤로 SetHUDVisible(true) → ClosePage(GameAndUI)
ClosePage() 를 처음 설계할 때 복원 모드를 GameOnly 로 고정하려 했다. 화면이 없으니 게임 입력만 있으면 된다고 생각했다.
프로젝트 설정을 확인하니 아니었다.
UCWUIBootstrapComponent::InitialInputMode C++ 기본값 = GameOnly
BP_TestInPlayerController GameAndUI 로 덮어씀 ← 실제 사용값
그리고 ApplyInputMode(GameOnly) 는 이걸 한다.
PlayerController->SetShowMouseCursor(false);
이 게임은 패링이 커서 위치를 쓰고 작업도 커서 트레이스다. 설정을 닫는 순간 둘 다 죽는다.
그래서 복원 모드를 호출자가 지정하도록 파라미터로 받았다. 기본값을 GameAndUI 로 두면 대부분의 호출에서 고민할 필요가 없다.
화면을 닫는 함수가 「원래 상태」를 안다고 가정하면 안 된다.
원래 상태는 화면이 아니라 게임 쪽이 정한다.
설정 화면을 열면 현재 값을 콤보박스에 반영해야 한다. 그런데 Set Selected Index 는 On Selection Changed 를 발생시킨다.
가드가 없으면 이렇게 된다.
현재 품질이 Cinematic(4)
→ 설정 화면 열기
→ 역매핑으로 「높음」 선택 → Set Selected Index(2)
→ On Selection Changed 발화
→ Epic(3) 적용 + 저장
아무것도 안 눌렀는데 화질이 내려가고 그 상태로 저장된다. 해상도도 같다. 현재 해상도가 지원 목록에 없으면 엉뚱한 값이 적용된다.
Set bIsInitializing = true
→ 현재 설정 읽어서 콤보박스에 반영
→ Set bIsInitializing = false
각 On Selection Changed 첫 줄에서 이 플래그를 확인하고 참이면 아무것도 하지 않는다.
이 가드에는 함정이 하나 더 있다. 목록 갱신 함수 중간에서 조기 반환하면 bIsInitializing 이 참으로 남는다.
그러면 그 뒤로 모든 설정 변경이 조용히 무시된다. 화면은 멀쩡한데 콤보박스를 아무리 돌려도 아무 일이 안 일어나고, 그 세션 내내 그렇다.
블루프린트에서는 「중간 Return Node 를 만들지 않는다」로 정리했다. 정확히는 모든 실행 경로가 마지막 해제를 지나가야 한다.
UI Root 의 레이어 순서는 이렇다.
Overlay_ScreenLayer ← 설정 화면이 여기
Overlay_HUDLayer ← 인게임 HUD 가 여기. 위에 그려진다
Overlay_ToastLayer
Overlay_PopupLayer
Overlay_TransitionLayer
클리어 후 Result 화면을 만들 때 이미 한 번 겪은 문제다. 그때는 WBP_InGameHUD 가 자기 자신을 접는 방식으로 해결했는데, 그건 클리어 상태에 묶여 있어서 재사용이 안 됐다.
이번에는 레이어 자체를 접는 함수를 뒀다.
void UCWUIRootWidget::SetHUDLayerVisible(bool bVisible)
{
Overlay_HUDLayer->SetVisibility(
bVisible
? ESlateVisibility::SelfHitTestInvisible
: ESlateVisibility::Collapsed
);
}
복원값이 Visible 이면 안 된다. HUD 가 클릭을 먹어서 아래 Screen 레이어의 버튼이 안 눌린다.
여는 순서도 정했다.
ESC
→ SetHUDVisible(false)
→ OpenPage(Settings)
→ 반환값이 false 면 SetHUDVisible(true)
먼저 숨기고 실패하면 되돌리는 쪽이, 열고 나서 숨기는 것보다 레이어 겹침을 덜 만든다.
창 모드를 바꾸면 현재 해상도의 의미도 바뀐다. 그래서 창 모드 변경 뒤에 해상도 목록을 다시 초기화해야 하는데, 바로 읽으면 갱신 전 값을 잡는다.
Apply Settings 가 뷰포트를 실제로 바꾸는 건 다음 프레임이다.
Set Fullscreen Mode
→ ApplyAndSave
→ Delay Until Next Tick
→ RefreshResolutionOptions
Delay Until Next Tick 은 latent 노드라 블루프린트 함수 안에 놓을 수 없다. 이벤트 그래프에만 배치된다. 지연은 이벤트 그래프에 남기고 실제 작업만 함수로 빼야 한다.
뷰포트가 재생성되면 키보드 포커스가 풀린다. 창 모드를 한 번 바꾸고 나면 ESC 로 설정이 안 닫힌다.
그래서 지연 뒤에 포커스도 같이 복원한다. 대상은 루트 위젯이 아니라 실제 버튼으로 잡았다.
Delay Until Next Tick
→ Is Valid(self)
→ RefreshResolutionOptions
→ Is Valid(Button_Back)
→ Set User Focus(Button_Back)
「뒤로」 버튼을 고른 이유가 있다. 포커스가 버튼에 가면 Enter 로도 눌린다. 「로비로 이동」에 포커스를 줬다면 무심코 Enter 를 눌러 판이 날아간다. 최악의 경우가 「뒤로 나가짐」인 쪽이 안전하다.
그리고 Set User Focus 는 대상이 Collapsed 면 실패한다. 「로비로 이동」은 인게임에서만 보이는 조건부 버튼이라 애초에 대상으로 부적합하다.
UGameUserSettings 가 적용·저장·복원을 전부 처리하므로 C++ 없이 블루프린트만으로 만들 수 있다. 스케일러빌리티는 엔진 전역이라 앞으로 추가될 에셋에도 자동으로 걸린다.| 팀프로젝트 12. 환경설정 화면 구현 (0) | 2026.08.19 |
|---|---|
| 팀프로젝트 11. ViewModel 기반 클래스를 생성 (0) | 2026.08.18 |
| 팀프로젝트 09. 클리어 후 방 복귀와 연결이 끊긴 뒤의 정리 (0) | 2026.08.13 |
| 팀프로젝트 08. 결과 화면 전환과 HUD 레이어 정리 (0) | 2026.08.12 |
| 팀프로젝트 07. 리슨 서버에서 "나"를 찾는 법, 인게임 HUD 플레이어 목록 연동 (0) | 2026.08.11 |