지난 글에서 환경설정 화면과 인게임 메뉴를 설계했다. 이번에는 그걸 실제로 구현했고,
덤으로 그동안 개인 개발 폴더에 방치해뒀던 맵과 블루프린트를 정식 위치로 옮겼다.
설계대로 된 건 절반쯤이다. 나머지 절반은 만들면서 바뀌었다.
이 글은 뭘 만들었는지보다 왜 계획이 바뀌었는지에 무게를 뒀다.
| 주제 | 문제 | 해결 |
|---|---|---|
| 이동 입력 | 설정을 닫아도 캐릭터가 계속 걸어감 | Set Ignore Move Input 제거하고 FlushPressedKeys 로 교체 |
| 초기화 가드 | 화면을 열기만 해도 설정이 저장됨 | bIsInitializing 소유권을 호출부로 통일 |
| 그래픽 품질 | 3단계와 엔진 5단계가 안 맞음 | 4단계로 늘려 인덱스를 엔진 값과 1:1 |
| 포커스 | 공용 버튼 위젯이 포커스를 못 받음 | Set User Focus + 전체 화면 Backdrop |
| 화면 닫기 | 메뉴와 인게임에서 동작이 갈림 | 세션 유무로 분기하고 공통 함수로 통합 |
해상도, 창 모드, 그래픽 품질은 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 이면 저장·복원까지 다 확인된다.
콤보박스에 현재 값을 표시하려고 Set Selected Index 를 부르면On Selection Changed 가 같이 발생한다.
그러면 이런 일이 벌어진다.
설정 화면을 연다
→ 현재 해상도를 콤보박스에 표시
→ On Selection Changed 발생
→ 사용자가 고른 줄 알고 Apply + Save
아무것도 안 눌렀는데 설정이 저장된다.
값 자체는 같으니 겉으로는 멀쩡해 보이는 게 더 나쁘다.
그래서 초기화 중인지를 나타내는 불리언 하나를 둔다.
On Selection Changed
→ Branch(bIsInitializing)
True → 아무것도 하지 않음
False → 실제 적용
여기서 처음에 잘못 잡았다.
각 초기화 함수가 자기 안에서 플래그를 켜고 끄게 만들었는데, 이러면 이렇게 된다.
RefreshResolutionOptions
→ true 켬 → 목록 채움 → false 끔 ← 여기서 꺼진다
InitializeWindowModeSelection ← 무방비
InitializeQualitySelection ← 무방비
먼저 도는 초기화만 보호받는다.
플래그는 전체 초기화를 시작하는 쪽이 한 번만 켜고 한 번만 꺼야 한다.
Event OnScreenOpened
→ Set bIsInitializing = true
→ RefreshResolutionOptions
→ InitializeWindowModeSelection
→ InitializeQualitySelection
→ Set bIsInitializing = false
→ Set User Focus
세 함수는 플래그를 건드리지 않는다.
이렇게 두면 나중에 초기화 항목이 늘어나도 사이에 끼워 넣기만 하면 된다.
콤보박스는 문자열만 담을 수 있어서 "1920 x 1080" 같은 형태로 넣게 된다.
그런데 적용할 때 이걸 다시 파싱하면, 구분자 하나 바뀌어도 깨진다.
표시용 문자열과 실제 값을 따로 들고 있으면 된다.
SupportedResolutions TArray<FIntPoint> 인덱스로 접근
ComboBoxString 표시용 문자열
Get Supported Fullscreen Resolutions 의 출력 핀에서Promote to Variable 로 변수를 만들면 타입 이름을 직접 안 적어도 된다.
엔진 버전마다 표시 이름이 달라서 손으로 고르면 틀리기 쉽다.
그리고 화면을 열 때마다 Clear Options 후 목록을 다시 만든다.
안 그러면 재진입할 때마다 항목이 두 배씩 늘어난다.
현재 해상도가 지원 목록에 없는 경우도 있다. 그때는 목록에 직접 추가한다.
없으면 인덱스가 -1 이 되어 아무것도 선택되지 않은 상태로 보인다.
처음 계획은 낮음 / 중간 / 높음 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
4 나 -1 이 나올 때 어떻게 보여줄지가 남는다.
4 (Cinematic) → 매우 높음(3) 으로 표시 실제와 가장 가까움
그 외 (-1 등) → 중간(1) 로 표시 과대 표시하지 않는 쪽
-1 에 「높음」을 붙이면, 실제로는 저사양 설정인데 화면에는 높음이라고 뜬다.
사용자는 설정이 고장났다고 판단한다. 틀리더라도 낮게 틀리는 쪽이 낫다.
중요한 건 이때 표시만 하고 적용하거나 저장하지 않는 것이다.
설정 화면을 열었다는 이유만으로 기존 품질을 덮어쓰면 안 된다.
품질만 바꾸는데 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 를 안 하면 아무 일도 일어나지 않는다.
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 는 숨겨야 하므로 양쪽을 합류시켰다.
처음 설계는 HUD 를 먼저 숨기고 화면을 여는 것이었다.
그런데 OpenPage 가 실패하면 HUD 만 사라지고 아무것도 안 뜬다.
성공한 뒤에 숨기면 그럴 일이 없다.
환경설정은 두 군데에서 열린다.
메인메뉴에서 → 닫으면 메인메뉴로 돌아가야 함
인게임에서 → 닫으면 게임으로 돌아가야 함
판별은 세션 참가 여부로 한다.
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
한때 "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() 가 이미 하고 있다.
같은 대입을 두 군데 두면 나중에 한쪽만 고치는 일이 생긴다.
키보드로 화면을 닫으려면 위젯이 포커스를 갖고 있어야 한다.
그런데 처음 시도한 방법이 둘 다 걸렸다.
Set Keyboard Focus 는 어느 플레이어의 키보드인지 지정하지 않는다.
싱글이면 상관없지만 리슨 서버에서는 로컬 플레이어를 명시해야 한다.
Set User Focus
├ Target WBP_ButtonCommon_Back
└ Player Controller Get Owning Player
LogSlate: Warning: WBP_ButtonCommon_Back in WBP_Settings does not support focus
UUserWidget 은 Is Focusable 기본값이 false 다.
공용 버튼 위젯 안에 진짜 Button 이 들어 있어도, 껍데기에 포커스를 주면 무시된다.
Class Defaults → Interaction → Is Focusable 을 켜야 한다.
설정 화면에는 「로비로 이동」과 「뒤로가기」가 있다.
처음에는 첫 버튼에 포커스를 줬는데, 엔터를 무심코 누르면 방을 나가버린다.
포커스는 「뒤로가기」에 준다. 잘못 눌러도 화면만 닫힌다.
패널 바깥을 클릭하면 포커스가 빠져서 키 입력이 안 먹는다.
전체 화면을 덮는 버튼을 하나 깔고, 눌리면 설정으로 포커스를 되돌린다.
이 버튼이 배경을 어둡게 하는 역할까지 겸한다. 별도 Border 를 안 써도 된다.
다만 전체 화면 버튼이라 Hovered / Pressed 브러시가 Normal 과 다르면
마우스를 움직일 때마다 화면 전체가 깜빡인다. 셋을 같은 값으로 맞춰야 한다.
UGameUserSettings 가 다 갖고 있다. C++ 없이 된다.Set Selected Index 는 On Selection Changed 를 발생시킨다. 초기화 가드가 필요하고, 그 소유권은 호출부에 있어야 한다.Set Ignore Move Input 은 스택 카운터라 짝이 안 맞으면 영구히 막힌다. FlushPressedKeys 로 원인을 없애는 쪽이 맞다.Is Focusable 기본값이 false 다. 껍데기에 포커스를 주면 무시된다.| 팀프로젝트 14. 화면 동결과 공용 피드백 레이어 (0) | 2026.08.24 |
|---|---|
| 팀프로젝트 13. 체온 게이지와 해상도 표시 버그 (0) | 2026.08.21 |
| 팀프로젝트 11. ViewModel 기반 클래스를 생성 (0) | 2026.08.18 |
| 팀프로젝트 10. 환경설정 화면과 인게임 ESC 메뉴 설계 (0) | 2026.08.14 |
| 팀프로젝트 09. 클리어 후 방 복귀와 연결이 끊긴 뒤의 정리 (0) | 2026.08.13 |