UI는 플레이어에게 지금 상태를 알려 준다
지금까지 만든 수집 게임은 동전을 먹으면 점수가 올라간다. 하지만 화면에는 아직 그 결과가 제대로 보이지 않는다. Print String은 테스트에는 좋지만, 실제 게임 화면의 안내문으로 쓰기에는 부족하다.
이번 장에서는 UMG(Unreal Motion Graphics)를 사용한다. UMG는 언리얼 엔진에서 HUD, 메뉴, 버튼, 안내문 같은 화면 UI를 만드는 시스템이다. 여기서 HUD는 Heads-Up Display의 줄임말로, 게임 플레이 중 화면 위에 계속 보이는 정보 영역을 뜻한다. 점수, 체력, 미니맵, 목표 안내가 모두 HUD에 들어갈 수 있다.
이번 장에서 만들 UI는 네 가지다.
화면 왼쪽 위에 보이는 점수 HUD
화면 중앙이나 위쪽에 보이는 목표 안내문
Esc 또는 Pause 입력으로 여는 일시정지 메뉴
모든 동전을 모았을 때 보여 주는 게임 클리어 화면
UI는 예쁘게 꾸미는 일보다 먼저 명확하게 전달하는 일이 중요하다. 플레이어가 "무엇을 해야 하는지", "얼마나 진행했는지", "지금 게임이 멈췄는지"를 바로 알 수 있어야 한다.
AI 생성 이미지
Widget Blueprint 만들기
먼저 점수와 안내문을 표시할 HUD 위젯을 만든다.
Content Browser에서 UI 폴더를 만든다. 이미 폴더를 만들어 두었다면 그 안으로 이동한다. 빈 공간을 우클릭하고 User Interface > Widget Blueprint를 선택한다. 이름은 WBP_HUD로 정한다.
WBP_HUD를 열면 일반 블루프린트와 다른 화면이 나온다. 왼쪽에는 Palette, 가운데에는 Designer 화면, 오른쪽에는 Details 패널이 있다. 이곳에서는 노드를 먼저 연결하기보다 화면에 보일 요소를 배치한다. 나중에 버튼 클릭이나 점수 표시처럼 동작이 필요한 부분은 Graph에서 만든다.
가장 먼저 확인할 것은 Canvas Panel이다. Canvas Panel은 위젯 안의 넓은 배치판이다. 텍스트나 버튼을 원하는 위치에 놓을 수 있다. 만약 Hierarchy에 Canvas Panel이 없다면 Palette에서 Canvas Panel을 찾아 화면으로 드래그한다.
이제 Palette에서 Text 또는 Text Block을 Canvas Panel 위로 드래그한다. 이름은 Details 패널이나 Hierarchy에서 TXT_Score로 바꾼다. Text 값은 임시로 다음처럼 적어 둔다.
Coins: 0
위치는 화면 왼쪽 위로 옮긴다. 글자가 너무 작으면 Font Size를 28 정도로 키운다. 색은 흰색이나 노란색처럼 배경과 잘 구분되는 색이 좋다.
다음으로 목표 안내문을 하나 더 만든다. Text Block을 추가하고 이름을 TXT_Goal로 바꾼다. Text 값은 다음처럼 적는다.
동전을 모두 모으세요!
이 안내문은 화면 위쪽 가운데에 배치한다. 처음에는 대략 놓아도 괜찮다. 곧 Anchor로 정리할 것이다.
Anchor로 해상도에 흔들리지 않게 배치하기
UI를 만들 때 초보자가 자주 겪는 문제가 있다. 에디터에서는 왼쪽 위에 있던 점수가 실행 화면에서는 이상한 위치로 밀리거나, 해상도를 바꾸면 버튼이 화면 밖으로 나가는 문제다. 이때 확인해야 하는 것이 Anchor다.
Anchor는 UI 요소가 화면의 어느 기준점에 붙어 있을지 정하는 설정이다. 예를 들어 점수는 화면 왼쪽 위에 붙어 있어야 한다. 화면이 커져도 왼쪽 위에서 일정한 거리만 유지하면 된다. 반대로 목표 안내문은 화면 위쪽 가운데를 기준으로 있어야 자연스럽다.
TXT_Score를 선택한다. Details 패널의 Slot 항목에서 Anchor 아이콘을 찾는다. 점수 텍스트는 왼쪽 위 Anchor를 선택한다. 위치 값은 대략 X: 40, Y: 30 정도로 둔다.
TXT_Goal은 위쪽 가운데 Anchor를 선택한다. Alignment를 X: 0.5, Y: 0으로 맞추면 텍스트의 가운데가 기준점에 맞는다. 위치는 X: 0, Y: 30 정도로 둔다. 이렇게 하면 화면 폭이 달라져도 안내문이 위쪽 중앙에 남아 있다.
버튼이나 종료 화면처럼 화면 중앙에 있어야 하는 요소는 가운데 Anchor를 사용한다. UI가 어긋난다면 먼저 위치 숫자보다 Anchor가 맞는지 확인하자. 위치를 아무리 바꿔도 실행 화면에서 계속 밀린다면, 대부분 Anchor나 Alignment가 의도와 다르게 잡혀 있는 경우가 많다.
HUD를 화면에 띄우기
위젯은 만들기만 해서는 화면에 나오지 않는다. 게임이 시작될 때 위젯을 생성하고 Viewport에 추가해야 한다. Viewport는 플레이어가 실제로 보는 게임 화면이라고 생각하면 된다.
이번 책의 흐름에서는 BP_ThirdPersonCharacter에서 간단히 처리하자. 캐릭터 블루프린트를 열고 Event Graph로 이동한다. Event BeginPlay가 있다면 그 뒤에 다음 흐름을 추가한다.
Get Player Controller노드 추가Create Widget노드 추가Create Widget의 Class를WBP_HUD로 설정Create Widget의 Owning Player에Get Player Controller연결Return Value를 변수로 승격해
HUDWidget이라고 이름 붙이기Add to Viewport노드 연결
흐름은 다음과 같다.
Event BeginPlay → Get Player Controller → Create Widget(WBP_HUD) → Set HUDWidget → Add to Viewport
여기서 Owning Player를 연결해 두는 것이 좋다. 나중에 위젯 안에서 "이 UI를 보고 있는 플레이어가 누구인지" 찾을 때 필요하기 때문이다. 특히 잠시 뒤에 사용할 Get Owning Player Pawn은 위젯의 Owning Player가 제대로 잡혀 있어야 안정적으로 동작한다.
이제 Play를 누르면 화면에 Coins: 0과 안내문이 보인다. 만약 보이지 않는다면 먼저 Create Widget의 Class가 비어 있지 않은지, Add to Viewport까지 실행선이 연결되어 있는지, Owning Player에 Player Controller가 연결되어 있는지 확인한다.
Score 변수를 HUD에 표시하기
이제 이전 장에서 만든 Score 변수를 HUD에 연결하자. 가장 쉬운 방법은 Text Binding이다. 바인딩은 UI의 표시 값을 어떤 변수나 계산 결과와 연결하는 방식이다. 점수가 바뀌면 텍스트도 그 값을 읽어 화면에 보여 준다.
WBP_HUD를 다시 연다. TXT_Score를 선택하고 Details 패널에서 Text 항목을 찾는다. Text 오른쪽의 Bind 버튼을 누르고 Create Binding을 선택한다. 그러면 함수 그래프가 열린다.
이 함수는 Text Block이 화면에 표시할 문장을 돌려주는 역할을 한다. 다음 흐름으로 만든다.
Get Owning Player Pawn노드 추가Cast To BP_ThirdPersonCharacter연결Cast 성공 핀에서
Get Score가져오기Format Text노드 추가Format 값에
Coins: {Score}입력Score칸에 캐릭터의Score변수 연결Return Node의 Return Value에 Format Text 결과 연결
이제 동전을 먹으면 HUD의 숫자가 바뀐다.
만약 Binding 안에서 Cast가 실패한다면 두 가지를 먼저 확인한다. 첫째, WBP_HUD를 만들 때 Create Widget의 Owning Player에 Player Controller를 연결했는지 확인한다. 둘째, 실제 플레이어 Pawn이 BP_ThirdPersonCharacter인지 확인한다. 프로젝트나 템플릿에 따라 캐릭터 블루프린트 이름이 다를 수 있으니, 내 프로젝트에서 사용하는 캐릭터 이름에 맞게 Cast 대상을 바꿔야 한다.
조금 더 목표가 분명한 화면을 만들고 싶다면 캐릭터에 TargetScore Integer 변수를 추가하고 기본값을 전체 동전 개수로 넣는다. 예를 들어 레벨에 동전이 10개라면 TargetScore를 10으로 둔다. Format Text를 다음처럼 바꿀 수 있다.
Coins: {Score} / {Target}
이때 Score에는 캐릭터의 Score 변수를, Target에는 캐릭터의 TargetScore 변수를 연결한다.
이 방식은 초보자가 구조를 이해하기 쉽다. 다만 바인딩은 화면이 갱신될 때 자주 호출될 수 있다. 작은 프로젝트에서는 문제없지만, UI가 많아지는 큰 프로젝트에서는 점수가 바뀌는 순간에만 텍스트를 업데이트하는 방식이 더 효율적이다. 지금은 "변수 값을 UI가 읽어 보여 준다"는 감각을 익히는 데 집중하자.
일시정지 메뉴 만들기
게임에는 잠시 멈추는 기능이 필요하다. 새 Widget Blueprint를 만들고 이름을 WBP_PauseMenu로 정한다.
디자이너 화면에서 Canvas Panel을 준비한다. 그 위에 어두운 배경용 Border나 Image를 하나 깔고 화면 전체로 늘린다. 색은 검은색에 투명도를 약간 주면 좋다. 그 위에 세로 상자 역할을 하는 Vertical Box를 가운데에 배치한다.
Vertical Box 안에 Text Block과 Button 세 개를 넣는다.
Text Block:
일시정지Button:
계속하기Button:
처음부터 다시Button:
종료
각 Button 안에는 Text Block을 넣어 버튼 이름이 보이게 한다. 버튼 위젯 이름은 각각 BTN_Resume, BTN_Restart, BTN_Quit처럼 바꾸면 나중에 찾기 쉽다.
버튼을 클릭했을 때 실행할 동작은 Graph에서 만든다. 디자이너에서 BTN_Resume을 선택하고 Details 패널의 On Clicked 옆 + 버튼을 누른다.
계속하기 버튼의 흐름은 다음과 같다.
OnClicked → Get Owning Player → Set Game Paused(false) → Set Input Mode Game Only → Set Show Mouse Cursor(false) → Remove from Parent
Remove from Parent는 현재 위젯을 화면에서 제거하는 노드다. Set Game Paused(false)는 멈췄던 게임을 다시 움직이게 한다. 입력 모드를 Game Only로 바꾸면 UI 클릭보다 캐릭터 조작을 우선하게 된다. Set Input Mode Game Only와 Set Show Mouse Cursor의 Target에는 Player Controller가 필요하므로, 위젯에서는 보통 Get Owning Player를 연결하면 된다.
종료 버튼은 Quit Game 노드를 연결한다. 에디터에서 테스트할 때는 동작이 다르게 느껴질 수 있지만, 패키징된 게임에서는 게임을 종료하는 용도로 쓸 수 있다.
처음부터 다시 버튼은 현재 레벨을 다시 여는 방식으로 만들 수 있다. 가장 단순한 방법은 Open Level 노드에 현재 맵 이름을 넣는 것이다. 게임이 일시정지된 상태라면 Set Game Paused(false)를 먼저 실행한 뒤 Open Level을 연결하는 편이 안전하다. 맵 이름을 자주 바꿀 수 있다면 나중에 별도 변수로 관리하는 편이 좋다.
Pause 입력으로 메뉴 열기
이제 플레이 중에 일시정지 메뉴를 열어 보자. 이전 장에서 Enhanced Input을 다뤘으므로 같은 흐름으로 만들 수 있다.
Content Browser에서 Input Action을 새로 만들고 이름을 IA_Pause로 정한다. Value Type은 Boolean으로 둔다. IMC_Default를 열고 IA_Pause를 추가한 뒤 키를 Esc 또는 P로 배정한다.
BP_ThirdPersonCharacter에서 IA_Pause 이벤트를 추가한다. Boolean 입력은 키를 누르고 있는 동안 여러 번 실행될 수 있으므로, 여기서는 Triggered보다 Started 핀을 쓰는 편이 깔끔하다. Started 핀에서 다음 흐름을 만든다.
Get Player Controller노드 추가Create Widget으로WBP_PauseMenu생성Create Widget의 Owning Player에 Player Controller 연결Return Value를
PauseMenuWidget변수로 저장Add to ViewportSet Game Paused(true)Set Input Mode UI OnlySet Show Mouse Cursor(true)
게임을 멈추면 캐릭터 이동과 카메라 회전은 정지하고, 마우스로 버튼을 누를 수 있어야 한다. 그래서 입력 모드를 UI Only로 바꾸고 마우스 커서를 보이게 한다. Set Input Mode UI Only의 Target도 Player Controller이며, 가능하다면 In Widget to Focus에 방금 만든 WBP_PauseMenu를 연결해 메뉴가 바로 입력을 받을 수 있게 한다.
만약 Pause 키를 눌렀는데 메뉴가 여러 개 겹쳐서 생긴다면 PauseMenuWidget 변수가 이미 유효한지 확인한 뒤 생성하자. 예를 들어 Is Valid(PauseMenuWidget)이 True라면 새로 만들지 않고 흐름을 끝낸다. 처음에는 버튼으로 계속하기가 되는지만 확인해도 충분하지만, 메뉴가 겹치는 문제를 막아 두면 나중에 디버깅이 훨씬 편해진다.
게임 클리어 화면과 최종 결과 보여 주기
마지막으로 모든 동전을 모았을 때 게임 클리어 화면을 띄워 보자. 새 Widget Blueprint를 만들고 이름을 WBP_GameClear로 정한다.
화면 중앙에 Text Block을 배치하고 다음처럼 적는다.
게임 클리어!
그 아래에 최종 점수를 보여 줄 Text Block을 하나 더 만든다.
Final Score: 0
그리고 버튼 두 개를 둔다.
다시 시작
종료
다시 시작 버튼과 종료 버튼은 일시정지 메뉴에서 만든 방식과 거의 같다. 다시 시작은 Set Game Paused(false) 후 Open Level을 실행하고, 종료는 Quit Game을 실행하면 된다.
최종 점수를 표시하는 방식은 HUD와 비슷하게 바인딩으로 만들 수 있다. Final Score Text Block의 Text에 Binding을 만들고, Get Owning Player Pawn → Cast To BP_ThirdPersonCharacter → Get Score → Format Text 흐름으로 연결한다. Format 값은 예를 들어 다음처럼 적을 수 있다.
Final Score: {Score}
이제 BP_ThirdPersonCharacter의 AddScore 이벤트를 수정한다. 이전 장에서 AddScore는 동전을 먹을 때 Score를 증가시키는 역할을 했다. Score를 새 값으로 Set한 다음, 다음 조건을 추가한다.
Score >= TargetScore
Branch가 True라면 게임 클리어 위젯을 띄운다.
Get Player Controller → Create Widget(WBP_GameClear) → Add to Viewport → Set Game Paused(true) → Set Input Mode UI Only → Set Show Mouse Cursor(true)
이때도 Create Widget의 Owning Player에는 Player Controller를 연결한다. 그래야 게임 클리어 위젯 안에서 Get Owning Player Pawn으로 캐릭터를 찾기 쉽다. 또한 게임 클리어 위젯이 여러 번 뜨지 않게 하려면 bGameCleared 같은 Boolean 변수를 하나 만들고, 이미 클리어된 상태라면 더 이상 위젯을 만들지 않게 처리하면 좋다.
이 구조는 단순하지만 중요한 흐름을 담고 있다. 아이템은 자신이 몇 점짜리인지 알고 있고, 플레이어는 총점을 관리하며, UI는 플레이어의 상태를 화면에 보여 준다. 역할이 나뉘어 있으면 나중에 체력, 제한 시간, 열쇠 개수 같은 요소도 쉽게 추가할 수 있다.
자주 막히는 부분 확인하기
HUD가 화면에 보이지 않는다면 Add to Viewport가 실행되는지 확인한다. Widget Blueprint를 만든 것만으로는 화면에 나오지 않는다. Create Widget의 Class가 WBP_HUD로 설정되어 있는지도 함께 확인하자.
점수가 바뀌지 않는다면 Binding 안의 Cast 대상이 맞는지 확인한다. Third Person 템플릿을 사용 중이라면 플레이어 Pawn이 BP_ThirdPersonCharacter인지 확인해야 한다. 변수 이름이 Score가 아니라 다르게 되어 있다면 그 이름에 맞게 가져와야 한다. 또한 Get Owning Player Pawn을 사용했다면 Create Widget에서 Owning Player를 연결했는지도 확인한다.
일시정지 메뉴에서 마우스가 보이지 않는다면 Set Show Mouse Cursor(true)가 빠졌을 가능성이 크다. 버튼은 보이는데 클릭이 안 된다면 Set Input Mode UI Only 또는 Set Input Mode Game and UI가 Player Controller에 제대로 연결되어 있는지 확인한다.
Pause 키를 한 번 눌렀는데 메뉴가 여러 개 생긴다면 IA_Pause 이벤트에서 Triggered 대신 Started를 쓰고, PauseMenuWidget이 이미 유효한지 검사한다. UI가 한 번만 떠야 나중에 계속하기, 다시 시작, 종료 버튼도 예측하기 쉽게 동작한다.
UI 위치가 이상하다면 Anchor를 먼저 본다. 왼쪽 위 점수는 왼쪽 위 Anchor, 중앙 메뉴는 가운데 Anchor, 화면 아래 버튼은 아래쪽 Anchor처럼 기준점을 맞추면 해상도가 달라져도 덜 흔들린다.
이번 장에서 게임은 훨씬 명확해졌다. 이제 플레이어는 동전을 몇 개 모았는지 보고, 필요하면 게임을 멈추고, 목표를 달성하면 결과 화면을 확인할 수 있다. 다음 장에서는 여기에 사운드와 이펙트를 더해 동전을 먹는 순간의 반응감을 높여 보자.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.