상세 컨텐츠

본문 제목

팀프로젝트 10. 환경설정 화면과 인게임 ESC 메뉴 설계

Unreal C++

by hyunjunstar 2026. 8. 14. 23:21

본문

지난 글까지 방 흐름과 인게임 HUD 를 붙였다. 이번에는 환경설정 화면을 메인 메뉴와 인게임 양쪽에서 재사용하는 구조를 잡았다.

기능 자체는 크지 않은데, 기존 UI 구조에 없던 것 세 가지가 드러났다. 화면을 닫는 경로, 인게임 입력 모드 복원, HUD 레이어 제어다.

주제 문제 해결
해상도 · 그래픽 품질 직접 구현할 필요가 있나 언리얼 내장 지원 기능이라 C++ 0줄
화면 닫기 인게임에서 페이지를 닫을 경로가 없음 ClosePage() 추가
입력 모드 GameOnly 로 복원하면 커서가 사라짐 복원 모드를 파라미터로 받음
콤보박스 초기화 화면을 열기만 해도 설정이 바뀜 초기화 가드 플래그
HUD 설정 화면 위에 HUD 가 겹침 레이어 단위 표시 전환

1. 해상도와 그래픽 품질은 엔진이 이미 갖고 있다

1.1 UGameUserSettings

해상도와 그래픽 품질 설정은 언리얼 내장 지원 기능이다. 별도 저장 시스템이나 적용 코드를 만들 필요가 없다.

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 에 기록되고 다음 실행 때 자동으로 복원된다.

그래픽 품질이 특히 값이 좋다. 스케일러빌리티는 그림자 해상도·뷰 디스턴스·텍스처 밉·이펙트·포스트프로세싱 같은 엔진 전역 설정이라 에셋마다 붙이는 게 아니다. 지금 넣어두면 앞으로 들어올 맵·캐릭터·도구에 전부 걸린다.

1.2 3단계로 줄이면 인덱스가 어긋난다

엔진 레벨은 다섯 단계다.

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 에 넣으면 범위를 벗어난다.

2. 인게임에서 화면을 닫을 방법이 없었다

2.1 OpenPage(None) 은 실패한다

지금까지 화면 전환은 전부 다른 화면으로 바꾸는 것이었다. Title → MainMenu → Room → Result 처럼.

그런데 인게임 ESC 메뉴는 다르다. 닫으면 화면이 없는 상태로 돌아가야 한다.

const TSubclassOf<UCWUIScreenWidget>* FoundClass = PageClasses.Find(TargetPage);

if (FoundClass == nullptr || !(*FoundClass))
{
    return false;
}

OpenPage(None) 은 PageClasses 에 None 이 없어서 여기서 실패한다. 화면을 정리하는 CloseCurrentPage() 는 있지만 private 이라 위젯에서 부를 수 없다.

「화면 없음」이 목적지가 되는 경우를 처음부터 상정하지 않았던 것이다.

2.2 ClosePage() 와 복원 입력 모드

공개 함수를 하나 추가했다.

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)

3. GameOnly 로 되돌리면 커서가 사라진다

ClosePage() 를 처음 설계할 때 복원 모드를 GameOnly 로 고정하려 했다. 화면이 없으니 게임 입력만 있으면 된다고 생각했다.

프로젝트 설정을 확인하니 아니었다.

UCWUIBootstrapComponent::InitialInputMode   C++ 기본값 = GameOnly
BP_TestInPlayerController                    GameAndUI 로 덮어씀   ← 실제 사용값

그리고 ApplyInputMode(GameOnly) 는 이걸 한다.

PlayerController->SetShowMouseCursor(false);

이 게임은 패링이 커서 위치를 쓰고 작업도 커서 트레이스다. 설정을 닫는 순간 둘 다 죽는다.

그래서 복원 모드를 호출자가 지정하도록 파라미터로 받았다. 기본값을 GameAndUI 로 두면 대부분의 호출에서 고민할 필요가 없다.

화면을 닫는 함수가 「원래 상태」를 안다고 가정하면 안 된다.
원래 상태는 화면이 아니라 게임 쪽이 정한다.

4. 화면을 열기만 해도 설정이 바뀐다

4.1 Set Selected Index 가 이벤트를 발생시킨다

설정 화면을 열면 현재 값을 콤보박스에 반영해야 한다. 그런데 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 첫 줄에서 이 플래그를 확인하고 참이면 아무것도 하지 않는다.

4.2 플래그는 모든 경로에서 내려야 한다

이 가드에는 함정이 하나 더 있다. 목록 갱신 함수 중간에서 조기 반환하면 bIsInitializing 이 참으로 남는다.

그러면 그 뒤로 모든 설정 변경이 조용히 무시된다. 화면은 멀쩡한데 콤보박스를 아무리 돌려도 아무 일이 안 일어나고, 그 세션 내내 그렇다.

블루프린트에서는 「중간 Return Node 를 만들지 않는다」로 정리했다. 정확히는 모든 실행 경로가 마지막 해제를 지나가야 한다.

5. HUD 레이어가 설정 화면 위에 그려진다

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)

먼저 숨기고 실패하면 되돌리는 쪽이, 열고 나서 숨기는 것보다 레이어 겹침을 덜 만든다.

6. 뷰포트가 바뀌면 한 프레임 뒤에 읽어야 한다

6.1 적용 직후에 읽으면 이전 값이 나온다

창 모드를 바꾸면 현재 해상도의 의미도 바뀐다. 그래서 창 모드 변경 뒤에 해상도 목록을 다시 초기화해야 하는데, 바로 읽으면 갱신 전 값을 잡는다.

Apply Settings 가 뷰포트를 실제로 바꾸는 건 다음 프레임이다.

Set Fullscreen Mode
→ ApplyAndSave
→ Delay Until Next Tick
→ RefreshResolutionOptions

Delay Until Next Tick 은 latent 노드라 블루프린트 함수 안에 놓을 수 없다. 이벤트 그래프에만 배치된다. 지연은 이벤트 그래프에 남기고 실제 작업만 함수로 빼야 한다.

6.2 포커스도 같이 잃는다

뷰포트가 재생성되면 키보드 포커스가 풀린다. 창 모드를 한 번 바꾸고 나면 ESC 로 설정이 안 닫힌다.

그래서 지연 뒤에 포커스도 같이 복원한다. 대상은 루트 위젯이 아니라 실제 버튼으로 잡았다.

Delay Until Next Tick
→ Is Valid(self)
→ RefreshResolutionOptions
→ Is Valid(Button_Back)
→ Set User Focus(Button_Back)

「뒤로」 버튼을 고른 이유가 있다. 포커스가 버튼에 가면 Enter 로도 눌린다. 「로비로 이동」에 포커스를 줬다면 무심코 Enter 를 눌러 판이 날아간다. 최악의 경우가 「뒤로 나가짐」인 쪽이 안전하다.

그리고 Set User Focus 는 대상이 Collapsed 면 실패한다. 「로비로 이동」은 인게임에서만 보이는 조건부 버튼이라 애초에 대상으로 부적합하다.

7. 정리

  1. 해상도와 그래픽 품질은 언리얼 내장 지원 기능이다. UGameUserSettings 가 적용·저장·복원을 전부 처리하므로 C++ 없이 블루프린트만으로 만들 수 있다. 스케일러빌리티는 엔진 전역이라 앞으로 추가될 에셋에도 자동으로 걸린다.
  2. UI 항목 수를 줄이면 인덱스가 곧 값이 아니게 된다. 5단계를 3단계로 줄이는 순간 양방향 매핑이 필요해진다. 그리고 축약한 최상단은 원래 기본값과 같은 곳을 가리켜야, 설정을 여는 것만으로 상태가 바뀌지 않는다.
  3. 화면을 닫는 경로는 여는 경로와 별개로 설계해야 한다. 전환만 상정한 구조에서는 「화면 없음」이 목적지가 될 수 없다.
  4. 화면을 닫는 함수가 원래 입력 모드를 안다고 가정하면 안 된다. 커서가 필요한 게임인지 아닌지는 UI 가 아니라 게임 쪽이 정한다. 호출자가 지정하게 두는 편이 안전하다.
  5. UI 값을 코드로 채울 때는 변경 이벤트가 같이 발생한다. 초기화 구간을 플래그로 감싸지 않으면 화면을 여는 것만으로 설정이 바뀌고 저장된다. 그리고 그 플래그는 모든 실행 경로에서 내려야 한다. 한 경로만 빠져도 이후 모든 변경이 조용히 무시된다.
  6. 레이어 순서는 코드가 아니라 위젯 계층이 결정한다. 같은 문제가 두 번째 나왔다면 개별 위젯이 아니라 레이어 단위로 제어하는 함수를 두는 게 맞다.
  7. 뷰포트를 바꾸는 설정은 적용이 다음 프레임에 일어난다. 바로 읽으면 이전 값이 나오고, 포커스도 함께 잃는다. 지연 뒤에 상태와 포커스를 같이 복원해야 한다.

관련글 더보기