상세 컨텐츠

본문 제목

팀프로젝트 12. 환경설정 화면 구현

Unreal C++

by hyunjunstar 2026. 8. 19. 22:46

본문

지난 글에서 환경설정 화면과 인게임 메뉴를 설계했다. 이번에는 그걸 실제로 구현했고,
덤으로 그동안 개인 개발 폴더에 방치해뒀던 맵과 블루프린트를 정식 위치로 옮겼다.

설계대로 된 건 절반쯤이다. 나머지 절반은 만들면서 바뀌었다.
이 글은 뭘 만들었는지보다 왜 계획이 바뀌었는지에 무게를 뒀다.

주제 문제 해결
이동 입력 설정을 닫아도 캐릭터가 계속 걸어감 Set Ignore Move Input 제거하고 FlushPressedKeys 로 교체
초기화 가드 화면을 열기만 해도 설정이 저장됨 bIsInitializing 소유권을 호출부로 통일
그래픽 품질 3단계와 엔진 5단계가 안 맞음 4단계로 늘려 인덱스를 엔진 값과 1:1
포커스 공용 버튼 위젯이 포커스를 못 받음 Set User Focus + 전체 화면 Backdrop
화면 닫기 메뉴와 인게임에서 동작이 갈림 세션 유무로 분기하고 공통 함수로 통합

1. 환경설정은 대부분 엔진이 이미 갖고 있다

해상도, 창 모드, 그래픽 품질은 UGameUserSettings 가 전부 들고 있다.
C++ 를 한 줄도 안 써도 된다.

Get Game User Settings
├ Set Screen Resolution
├ Set Fullscreen Mode
├ Set Overall Scalability Level
├ Apply Settings              전체 적용
├ Apply Non Resolution Settings   해상도 빼고 적용
└ Save Settings               ini 로 기록

저장 위치는 실행 방식에 따라 다르다.

Standalone   <프로젝트>/Saved/Config/WindowsEditor/GameUserSettings.ini
패키징        %LOCALAPPDATA%/<프로젝트>/Saved/Config/Windows/GameUserSettings.ini

여기서 중요한 함정이 하나 있다.
PIE 에서는 해상도와 창 모드가 아무 일도 하지 않는다.
게임이 에디터 뷰포트 안에 얹혀 있어서 창이 게임 소유가 아니기 때문이다.

처음에 "적용이 안 된다"고 한참 헤맸는데, Standalone 으로 띄우니 한 번에 됐다.
패키징까지 갈 필요는 없다. Standalone 이면 저장·복원까지 다 확인된다.

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

콤보박스에 현재 값을 표시하려고 Set Selected Index 를 부르면
On Selection Changed 가 같이 발생한다.

그러면 이런 일이 벌어진다.

설정 화면을 연다
→ 현재 해상도를 콤보박스에 표시
→ On Selection Changed 발생
→ 사용자가 고른 줄 알고 Apply + Save

아무것도 안 눌렀는데 설정이 저장된다.
값 자체는 같으니 겉으로는 멀쩡해 보이는 게 더 나쁘다.

그래서 초기화 중인지를 나타내는 불리언 하나를 둔다.

On Selection Changed
→ Branch(bIsInitializing)
   True  → 아무것도 하지 않음
   False → 실제 적용

1.2 가드의 소유권은 호출부에 있어야 한다

여기서 처음에 잘못 잡았다.
각 초기화 함수가 자기 안에서 플래그를 켜고 끄게 만들었는데, 이러면 이렇게 된다.

RefreshResolutionOptions
   → true 켬 → 목록 채움 → false 끔     ← 여기서 꺼진다

InitializeWindowModeSelection            ← 무방비
InitializeQualitySelection               ← 무방비

먼저 도는 초기화만 보호받는다.

플래그는 전체 초기화를 시작하는 쪽이 한 번만 켜고 한 번만 꺼야 한다.

Event OnScreenOpened
→ Set bIsInitializing = true
→ RefreshResolutionOptions
→ InitializeWindowModeSelection
→ InitializeQualitySelection
→ Set bIsInitializing = false
→ Set User Focus

세 함수는 플래그를 건드리지 않는다.
이렇게 두면 나중에 초기화 항목이 늘어나도 사이에 끼워 넣기만 하면 된다.

1.3 해상도 목록은 문자열로 다루지 않는다

콤보박스는 문자열만 담을 수 있어서 "1920 x 1080" 같은 형태로 넣게 된다.
그런데 적용할 때 이걸 다시 파싱하면, 구분자 하나 바뀌어도 깨진다.

표시용 문자열과 실제 값을 따로 들고 있으면 된다.

SupportedResolutions   TArray<FIntPoint>   인덱스로 접근
ComboBoxString         표시용 문자열

Get Supported Fullscreen Resolutions 의 출력 핀에서
Promote to Variable 로 변수를 만들면 타입 이름을 직접 안 적어도 된다.
엔진 버전마다 표시 이름이 달라서 손으로 고르면 틀리기 쉽다.

그리고 화면을 열 때마다 Clear Options 후 목록을 다시 만든다.
안 그러면 재진입할 때마다 항목이 두 배씩 늘어난다.

현재 해상도가 지원 목록에 없는 경우도 있다. 그때는 목록에 직접 추가한다.
없으면 인덱스가 -1 이 되어 아무것도 선택되지 않은 상태로 보인다.

2. 그래픽 품질은 3단계가 아니라 4단계로

처음 계획은 낮음 / 중간 / 높음 3단계였다.
엔진은 5단계(0 Low ~ 4 Cinematic)라서 이렇게 매핑하려 했다.

낮음 → 0
중간 → 1
높음 → 3 (Epic)

적용은 되는데 읽어올 때가 문제였다.

GetOverallScalabilityLevel() 이 2 를 반환하면 대응하는 항목이 없다.
4 도, 항목별로 값이 다를 때 나오는 -1 도 마찬가지다.
콤보박스 인덱스가 -1 이 되거나 엉뚱한 항목이 선택된 채로 보인다.

그래서 표시 단계를 엔진과 1:1 로 맞췄다.

0 낮음      → 엔진 0 (Low)
1 중간      → 엔진 1 (Medium)
2 높음      → 엔진 2 (High)
3 매우 높음  → 엔진 3 (Epic)

Cinematic(4) 은 일반 플레이 옵션으로 제공하지 않는다. 영상 촬영용이라 비용이 너무 크다.

인덱스와 엔진 값이 같아지니 Switch on Int 가 통째로 필요 없어졌다.

InitializeQualitySelection
→ Get Overall Scalability Level → Level
→ Branch(In Range(Level, 0, 3))
   True  → Set Selected Index(Level)
   False → Set Selected Index(Select Int(A=3, B=1, Pick A = Level == 4))
On Selection Changed
→ Branch(bIsInitializing)
   True  → 아무것도 하지 않음
   False → Branch(In Range(Get Selected Index, 0, 3))
            True → Set Overall Scalability Level(Index) → ApplyQualityAndSave

2.1 범위 밖 값은 표시만 하고 적용하지 않는다

4 나 -1 이 나올 때 어떻게 보여줄지가 남는다.

4 (Cinematic) → 매우 높음(3) 으로 표시    실제와 가장 가까움
그 외 (-1 등) → 중간(1) 로 표시           과대 표시하지 않는 쪽

-1 에 「높음」을 붙이면, 실제로는 저사양 설정인데 화면에는 높음이라고 뜬다.
사용자는 설정이 고장났다고 판단한다. 틀리더라도 낮게 틀리는 쪽이 낫다.

중요한 건 이때 표시만 하고 적용하거나 저장하지 않는 것이다.
설정 화면을 열었다는 이유만으로 기존 품질을 덮어쓰면 안 된다.

2.2 품질 전용 적용 함수

품질만 바꾸는데 Apply Settings 를 부르면 해상도와 창 모드까지 다시 적용된다.

ApplyQualityAndSave
→ Get Game User Settings
→ Apply Non Resolution Settings
→ Save Settings

Set Overall Scalability Level 은 구조체만 채우고 CVar 에는 반영하지 않는다.
Apply Non Resolution Settings 가 Scalability::SetQualityLevels 를 부르는 쪽이다.
Set 만 하고 Apply 를 안 하면 아무 일도 일어나지 않는다.

3. 설정을 열었더니 캐릭터가 계속 걸어갔다

W 를 누른 채로 설정을 열면 캐릭터가 멈추지 않는다.

입력 모드가 UI 전용으로 바뀌는 순간부터 게임은 키 이벤트를 못 받는다.
그래서 W 를 뗐다는 신호가 영영 안 온다. 눌린 상태가 그대로 남는다.

처음에는 Set Ignore Move Input 으로 막으려 했다. 이건 틀린 접근이었다.

Set Ignore Move Input(true)   내부 카운터 +1
Set Ignore Move Input(false)  내부 카운터 -1

스택 카운터라서 켜고 끄는 횟수가 한 번만 어긋나면 이동이 영구히 막힌다.
설정을 여는 도중에 다른 경로로 화면이 닫히기라도 하면 그대로 끝이다.

원인 자체를 없애는 쪽이 맞다.

void UCWUISubsystem::ApplyInputMode(ECWUIInputMode InInputMode)
{
    // ...

    // 입력 모드가 바뀌면 기존 키 해제 이벤트를 받지 못할 수 있으므로
    // Enhanced Input에 남아 있는 눌림 상태를 먼저 초기화
    PlayerController->FlushPressedKeys();

    // ...
}

여기에 이미 쌓인 이동 관성만 추가로 끊어주면 된다.

1 (Pressed)
→ Open Page(Settings)
→ Branch
   True → Is Valid(Get Controlled Pawn)
            Is Valid     → Stop Movement Immediately → Set HUDVisible(false)
            Is Not Valid → Set HUDVisible(false)
   False → 아무것도 하지 않음

Get Controlled Pawn 을 검사하는 이유는, 폰이 아직 안 만들어진 시점에
설정을 열면 Accessed None 이 나기 때문이다.
그리고 어느 경로로 가든 HUD 는 숨겨야 하므로 양쪽을 합류시켰다.

3.1 HUD 를 숨기는 시점

처음 설계는 HUD 를 먼저 숨기고 화면을 여는 것이었다.
그런데 OpenPage 가 실패하면 HUD 만 사라지고 아무것도 안 뜬다.

성공한 뒤에 숨기면 그럴 일이 없다.

4. 화면을 닫는 경로는 둘이다

환경설정은 두 군데에서 열린다.

메인메뉴에서   → 닫으면 메인메뉴로 돌아가야 함
인게임에서     → 닫으면 게임으로 돌아가야 함

판별은 세션 참가 여부로 한다.

CloseSettings
→ Is Valid(CWSessionViewModel)
   Is Valid → Branch(Is Empty(Get Current Room Code))
                True  → Request Open Page(MainMenu)
                False → CloseInGameSettings
   Is Not Valid → CloseInGameSettings

인게임 닫기가 두 군데에서 불리다 보니 처음에는 로직이 복사돼 있었다.
그러다 한쪽에만 포커스 복원이 들어가서 동작이 갈렸다.
공통 함수로 뺐다.

CloseInGameSettings
→ Close Page(Return Input Mode = Game and UI)
→ Branch
   True  → Set Focus to Game Viewport → Set HUDVisible(true)
   False → Print String

4.1 실패했을 때 HUD 를 먼저 복원하면 안 된다

한때 "HUD 를 먼저 켜고 닫자"는 안이 나왔다. 이건 안 된다.

UI 루트의 레이어 순서가 이렇게 되어 있다.

Overlay_ScreenLayer       ← 환경설정이 여기
Overlay_HUDLayer          ← HUD 가 여기, Screen 보다 위
Overlay_ToastLayer
Overlay_PopupLayer
Overlay_TransitionLayer

HUD 가 Screen 보다 위에 그려진다.
ClosePage 가 실패하면 설정 화면이 그대로 남는데, 그 위에 HUD 를 덮어버리면
설정 화면이 HUD 뒤로 가려진 채 입력만 살아 있는 상태가 된다.

ClosePage 는 실제 제거 전에만 실패하도록 되어 있다.

bool UCWUISubsystem::ClosePage(ECWUIInputMode ReturnInputMode)
{
    // 전환 중이거나 열린 페이지가 없으면 닫기 요청을 무시
    if (bIsTransitioning || CurrentPage == ECWUIPage::None)
    {
        return false;
    }

    BeginTransition();

    CloseCurrentPage();
    ApplyInputMode(ReturnInputMode);

    EndTransition();

    UE_LOG(LogTemp, Log, TEXT("UISubsystem: page closed."));

    return true;
}

그러니 성공 분기에서만 HUD 를 복원하는 게 맞다.

참고로 CurrentPage = None 은 여기 없다. CloseCurrentPage() 가 이미 하고 있다.
같은 대입을 두 군데 두면 나중에 한쪽만 고치는 일이 생긴다.

5. 포커스를 잡는 게 생각보다 까다로웠다

키보드로 화면을 닫으려면 위젯이 포커스를 갖고 있어야 한다.
그런데 처음 시도한 방법이 둘 다 걸렸다.

5.1 Set Keyboard Focus 가 아니라 Set User Focus

Set Keyboard Focus 는 어느 플레이어의 키보드인지 지정하지 않는다.
싱글이면 상관없지만 리슨 서버에서는 로컬 플레이어를 명시해야 한다.

Set User Focus
├ Target            WBP_ButtonCommon_Back
└ Player Controller Get Owning Player

5.2 위젯 블루프린트는 기본적으로 포커스를 안 받는다

LogSlate: Warning: WBP_ButtonCommon_Back in WBP_Settings does not support focus

UUserWidget 은 Is Focusable 기본값이 false 다.
공용 버튼 위젯 안에 진짜 Button 이 들어 있어도, 껍데기에 포커스를 주면 무시된다.

Class Defaults → Interaction → Is Focusable 을 켜야 한다.

5.3 포커스 대상은 안전한 쪽으로

설정 화면에는 「로비로 이동」과 「뒤로가기」가 있다.
처음에는 첫 버튼에 포커스를 줬는데, 엔터를 무심코 누르면 방을 나가버린다.

포커스는 「뒤로가기」에 준다. 잘못 눌러도 화면만 닫힌다.

5.4 바깥을 클릭하면 포커스가 사라진다

패널 바깥을 클릭하면 포커스가 빠져서 키 입력이 안 먹는다.
전체 화면을 덮는 버튼을 하나 깔고, 눌리면 설정으로 포커스를 되돌린다.

이 버튼이 배경을 어둡게 하는 역할까지 겸한다. 별도 Border 를 안 써도 된다.

다만 전체 화면 버튼이라 Hovered / Pressed 브러시가 Normal 과 다르면
마우스를 움직일 때마다 화면 전체가 깜빡인다. 셋을 같은 값으로 맞춰야 한다.

6. 정리

  1. 해상도·창 모드·품질은 UGameUserSettings 가 다 갖고 있다. C++ 없이 된다.
  2. PIE 에서는 해상도와 창 모드가 적용되지 않는다. Standalone 으로 확인한다.
  3. Set Selected Index 는 On Selection Changed 를 발생시킨다. 초기화 가드가 필요하고, 그 소유권은 호출부에 있어야 한다.
  4. 표시 단계를 엔진 값과 1:1 로 맞추면 매핑 코드가 통째로 사라진다.
  5. Set Ignore Move Input 은 스택 카운터라 짝이 안 맞으면 영구히 막힌다. FlushPressedKeys 로 원인을 없애는 쪽이 맞다.
  6. HUD 는 Screen 레이어보다 위에 그려진다. 닫기에 실패했을 때 HUD 를 복원하면 설정 화면이 뒤로 가려진다.
  7. 위젯 블루프린트는 Is Focusable 기본값이 false 다. 껍데기에 포커스를 주면 무시된다.

관련글 더보기