UI와 게임플레이 기본 상호작용
이벤트 디스패처와 함수 호출·데이터 바인딩으로 UI 입력과 게임 상태를 양방향 연결하고 입력 모드를 설정합니다.
화면 아래의 스킬 버튼을 눌렀는데 캐릭터가 아무 반응도 하지 않습니다. 반대로 캐릭터가 공격을 맞아 체력이 줄었는데 체력 바는 그대로입니다. 위젯을 예쁘게 만들고 애니메이션까지 넣었어도, UI와 게임 로직이 연결되어 있지 않으면 이런 일이 생깁니다. UI는 정보를 보여 주는 데서 끝나지 않고, 플레이어의 입력을 게임 상태로 전달하고 게임 상태의 변화를 화면에 보여 주는 양방향 통로가 되어야 합니다.
이 글에서는 두 방향을 하나씩 연결합니다. UI의 버튼 입력은 이벤트 디스패처와 직접 함수 호출로 게임플레이 블루프린트에 보내고, 체력 같은 게임 상태는 데이터 바인딩과 UI 함수 호출로 화면에 반영합니다. 마지막으로 입력이 UI와 게임 중 어디로 가는지를 정하는 입력 모드를 살펴봅니다.
UI와 게임플레이가 주고받는 두 방향
첫째 방향은 플레이어의 입력을 게임플레이에 전달하는 것입니다. 버튼 클릭, 슬라이더 조작, 텍스트 입력이 게임에 영향을 주어야 하므로, 게임 시작 버튼을 누르면 레벨을 불러오고 음량 슬라이더를 움직이면 게임 사운드가 바뀌어야 합니다.
둘째 방향은 게임 상태의 변화를 UI에 반영하는 것입니다. 체력이 줄거나, 아이템을 얻거나, 퀘스트를 완료하면 체력 바와 인벤토리 목록, 퀘스트 로그가 그에 맞게 바뀌어야 합니다. 두 방향이 매끄럽게 이어질 때 플레이어는 게임이 자신의 행동에 바로 반응한다고 느낍니다.
UI의 입력을 게임플레이로 보내기
이벤트 디스패처로 알리기
이벤트 디스패처(Event Dispatcher)는 어떤 일이 일어났다고 알리면, 그 알림에 등록해 둔 여러 곳이 각자 반응할 수 있게 하는 방송(broadcast) 방식의 기능입니다. 4장에서 액터 사이의 통신에 썼던 것과 같은 기능이며, UI에서 게임플레이로 이벤트를 보낼 때도 유용합니다. UI 위젯은 "버튼이 눌렸다"고 알리기만 하고, 누가 그 알림을 받아 무엇을 할지는 알 필요가 없기 때문입니다.
스킬 버튼을 누르면 플레이어가 스킬을 쓰는 예로 따라가 봅니다. 먼저 WBP_SkillButton 위젯 블루프린트를 만들고 Button을 추가한 뒤 Is Variable을 켜 이름을 SkillCastButton으로 정합니다. 그래프 탭의 마이 블루프린트(My Blueprint) 패널에서 이벤트 디스패처 목록의 추가 버튼으로 OnSkillButtonClicked 디스패처를 만들고, 어떤 스킬인지 알려 줄 SkillID(Integer) 입력을 추가합니다. 그다음 SkillCastButton의 On Clicked 이벤트 노드를 만들고, 디스패처를 그래프로 끌어 Call 노드를 만든 뒤 On Clicked의 실행 선을 연결해 SkillID 핀에 스킬 ID를 넣습니다. 컴파일하고 저장합니다.
알림을 받을 쪽은 BP_PlayerCharacter입니다. Event BeginPlay처럼 UI 위젯을 만드는 지점에서 Create Widget으로 WBP_SkillButton을 만들고, Return Value 핀에서 끌어 Assign OnSkillButtonClicked를 고릅니다. 디스패처 이름 앞에 Assign이 붙은 이 항목을 고르면 디스패처에 이벤트를 등록하는 Bind Event to OnSkillButtonClicked 노드와, 등록될 커스텀 이벤트가 함께 만들어집니다. 직접 구성하고 싶다면 Bind Event to OnSkillButtonClicked에 시그니처가 맞는 커스텀 이벤트를 연결해도 되지만, 두 방식을 함께 만들 필요는 없습니다. 이 커스텀 이벤트 뒤에 스킬을 쓰는 로직(예: Print String으로 "스킬 ID [SkillID] 발동!" 출력)을 잇고, 마지막으로 Add to Viewport로 위젯을 화면에 추가합니다.
이제 플레이하며 스킬 버튼을 누르면, WBP_SkillButton이 디스패처를 호출하고 BP_PlayerCharacter에 등록된 커스텀 이벤트가 실행되어 스킬 로직이 동작합니다.
게임플레이 블루프린트의 함수 직접 호출하기
UI 위젯이 플레이어 캐릭터, 플레이어 컨트롤러, 게임 모드 같은 게임플레이 블루프린트의 함수를 직접 호출하는 방법도 있습니다. 이 방법을 쓰려면 위젯이 그 블루프린트의 참조를 가지고 있어야 합니다.
인벤토리 창에서 아이템 사용 버튼을 누르면 플레이어 캐릭터의 함수를 호출하는 예를 봅니다. WBP_InventoryItem을 만들고 Button을 추가해 이름을 UseButton으로 정합니다. 그래프 탭에서 PlayerCharacterRef 변수(타입: BP_PlayerCharacter Object Reference)를 추가하고 Expose on Spawn과 Instance Editable을 켭니다. 이 변수에는 위젯을 만들 때 플레이어 캐릭터의 참조를 넣어 줄 것입니다. 그다음 UseButton의 On Clicked에서 PlayerCharacterRef를 Is Valid로 검사하고, 유효한 분기에서만 BP_PlayerCharacter에 정의된 UseItem(ItemData) 같은 함수를 호출해 ItemData 핀에 그 아이템의 정보를 연결합니다.
참조는 위젯을 만드는 쪽에서 넘겨줍니다. WBP_InventoryScreen에서 WBP_InventoryItem을 만드는 로직으로 가서 Create Widget의 Class에 WBP_InventoryItem을 고르면, Expose on Spawn을 켠 PlayerCharacterRef 입력 핀이 노드에 나타납니다. 로컬 플레이어의 폰을 얻어 BP_PlayerCharacter로 캐스트하고, 캐스트에 성공한 결과를 이 핀에 연결합니다. 기본 Pawn 타입의 참조를 사용자 정의 하위 타입의 핀에 그대로 연결할 수는 없으므로 캐스트가 필요합니다.
이 방법은 이벤트 디스패처보다 설정이 간단할 수 있지만, UI 위젯이 특정 게임플레이 블루프린트에 직접 의존하게 됩니다. WBP_InventoryItem은 이제 BP_PlayerCharacter가 없으면 동작하지 않으므로, 다른 캐릭터에서 같은 위젯을 쓰려면 위젯도 고쳐야 합니다. UI와 게임 로직의 결합이 그만큼 강해지는 셈입니다.
게임 상태를 UI에 반영하기
데이터 바인딩으로 체력 바 채우기
데이터 바인딩은 텍스트 내용이나 진행 바의 비율 같은 위젯 속성을, 그 값을 계산해 돌려주는 함수에 연결하는 방식입니다. 위젯 블루프린트의 기본 개념에서 본 것처럼 일반적인 UMG 속성 바인딩은 값이 바뀐 순간에만 호출되는 것이 아니라 값을 반복해서 읽어 표시합니다. 그래서 자주 갱신해야 하는 표시가 많다면, 상태가 바뀐 뒤에 Set Percent 같은 함수를 직접 호출하는 방식도 함께 검토합니다.
플레이어의 체력 바를 바인딩으로 만들어 봅니다. 먼저 BP_PlayerCharacter에 CurrentHealth(Float)와 MaxHealth(Float) 변수를 만들고, 피해를 받는 Event AnyDamage 같은 이벤트에서 CurrentHealth를 갱신합니다. 다음으로 WBP_HealthBar를 만들어 Progress Bar를 추가하고, 디테일 패널의 Percent 속성 옆 Bind 버튼에서 Create Binding을 고릅니다.
새로 만들어진 바인딩 함수(예: GetPercent_0)의 그래프에서는 위젯을 소유한 로컬 플레이어의 폰을 얻어 BP_PlayerCharacter로 캐스트합니다. 캐스트가 실패하면 표시할 기본값 0을 반환합니다. 성공하면 CurrentHealth와 MaxHealth를 읽고, MaxHealth가 0보다 큰 경로에서만 CurrentHealth / MaxHealth를 계산해 0에서 1 사이로 Clamp한 값을 반환합니다. 그렇지 않은 경로는 0을 반환해 0으로 나누는 일을 막습니다.
예를 들어 CurrentHealth가 75, MaxHealth가 100이면 Percent는 75 / 100 = 0.75이고, 체력 바가 4분의 3만큼 채워집니다. MaxHealth가 0이면 나눗셈을 하지 않고 앞에서 정한 기본값 0을 반환합니다. 이제 체력 변수가 바뀌면 바인딩 함수가 다시 읽을 때 Progress Bar가 새 비율로 표시됩니다. 다만 바인딩 함수와 이벤트에서 직접 Set Percent를 호출하는 흐름을 함께 쓰면 같은 속성을 두 곳에서 덮어쓰게 되므로, 한 속성에는 한 가지 방식만 쓰도록 설계합니다. 공식 문서도 바인딩한 속성을 위젯의 Set 함수로 직접 바꾸면 바인딩이 끊어진다고 안내합니다.
상태가 바뀔 때 UI 함수 호출하기
게임플레이 블루프린트가 UI 위젯의 함수를 직접 호출해 화면을 갱신하는 방법도 있습니다. 아이템을 얻었을 때 인벤토리 화면에 새 아이템을 추가하는 예를 봅니다.
WBP_InventoryScreen에 F_ItemData를 입력으로 받는 AddItemToUI 함수를 정의하고, 그 안에서 WBP_InventorySlot을 만들어 ItemData로 슬롯을 설정한 뒤 InventoryGrid에 추가합니다. 슬롯을 만들고 설정하는 방법은 동적 UI 요소 생성 및 기본 관리에서 만든 것과 같습니다. 다만 이 함수에는 반복의 Index가 없으므로, 추가하기 전에 InventoryGrid의 Get Children Count로 지금까지 들어 있는 슬롯 수를 구하고 그 값을 Index로 삼아 행과 열을 계산합니다.
BP_PlayerCharacter의 아이템 획득 로직에서는 위젯을 만든 로컬 컨트롤러에 저장해 둔 InventoryScreenRef를 가져옵니다. 화면에서 위젯을 검색해 찾는 노드 하나로 이 참조를 얻을 수 있다고 가정하지 않고, 만들 때 저장한 참조를 씁니다. 타입이 정해진 이 참조가 유효하면 AddItemToUI를 호출합니다. 화면을 아직 만들지 않았다면 게임 데이터만 유지해 두고, 나중에 화면을 열 때 최신 인벤토리로 초기화합니다.
입력 모드로 입력의 행선지 정하기
UI와 게임플레이를 연결할 때 입력 모드(input mode)도 함께 정해야 합니다. 입력 모드는 UI를 열 때 곁들이는 편의 기능이 아니라, 플레이어의 입력이 UI와 게임 가운데 어느 쪽으로 전달될지를 정하는 기준입니다. 언리얼 엔진은 세 가지 입력 모드를 제공합니다.
LOCAL INPUT · UI / GAME
UI가 소비한 입력은 Game And UI에서도 다시 전달되지 않습니다
입력 모드는 해당 로컬 Player Controller의 전달 경로를 정합니다. 커서 표시·포커스·메뉴 닫기와 이동/시점 제한도 각각 확인합니다.
| 모드 | 입력 처리 | 함께 설정할 것 |
|---|---|---|
| Game Only | Player Controller의 게임 입력 중심 | 필요한 커서 숨김과 게임 조작 복구 |
| UI Only | UI가 입력을 처리 | UI 내부의 닫기 입력, 포커스, 커서 표시 |
| Game And UI | UI가 먼저 처리하고 미처리 입력은 게임에 전달 | 어떤 키를 UI가 소비할지, 이동·시점 제한 여부 |
- 모드: Game Only
- 입력 처리: Player Controller의 게임 입력 중심
- 함께 설정할 것: 필요한 커서 숨김과 게임 조작 복구
- 모드: UI Only
- 입력 처리: UI가 입력을 처리
- 함께 설정할 것: UI 내부의 닫기 입력, 포커스, 커서 표시
- 모드: Game And UI
- 입력 처리: UI가 먼저 처리하고 미처리 입력은 게임에 전달
- 함께 설정할 것: 어떤 키를 UI가 소비할지, 이동·시점 제한 여부
커서 표시와 월드 일시정지는 입력 모드와 별도 설정입니다.
UI Only로 열었다면 UI에서 닫을 수 있어야 합니다
게임 쪽 키 이벤트에만 닫기를 맡기지 않습니다. UI 닫기 버튼이나 키 처리와 포커스를 제공하고, 닫을 때 게임 입력과 짝을 맞춘 이동·시점 제한을 복구합니다.
위 그림은 세 입력 모드에서 입력이 어디로 가는지와 함께 설정할 것을 표로 정리합니다. Game Only에서는 플레이어 컨트롤러의 게임 입력이 중심이 되며, 필요하면 커서를 숨기고 게임 조작을 복구합니다. UI Only에서는 UI가 입력을 처리하므로, UI 안에 닫기 입력과 포커스, 커서 표시를 갖춰야 합니다. Game And UI에서는 UI가 먼저 입력을 처리하고 UI가 처리하지 않은 입력만 게임에 전달되므로, 어떤 키를 UI가 소비할지와 이동이나 시점을 제한할지를 정해야 합니다.
그림의 제목처럼 UI가 소비한 입력은 Game And UI에서도 게임으로 다시 전달되지 않습니다. 또 입력 모드는 그 로컬 플레이어 컨트롤러의 전달 경로를 정할 뿐이며, 커서 표시와 월드 일시 정지는 입력 모드와 별개로 설정합니다. 그림 아래의 설명처럼 UI Only로 열었다면 UI에서 닫을 수 있어야 하므로, 게임 쪽 키 이벤트에만 닫기를 맡기지 말고 UI의 닫기 버튼이나 키 처리와 포커스를 제공합니다.
메뉴에만 집중시키려면 UI Only를, 채팅창처럼 게임 조작도 일부 유지하려면 Game And UI를 검토합니다. 일반적인 흐름은 게임을 시작할 때 Set Input Mode Game Only(필요하면 Game And UI)로 두고, 메뉴를 열 때 Set Input Mode UI Only로 바꾸며 마우스 커서를 보이게 하고, 메뉴를 닫을 때 다시 Set Input Mode Game Only로 돌리며 커서를 숨기는 것입니다.
입력 모드는 해당 로컬 플레이어 컨트롤러에 설정하고, 포커스할 위젯도 지정합니다. 메뉴를 닫을 때는 Remove from Parent, 게임 입력 모드, 커서 숨김과 함께, 메뉴를 열 때 걸었던 이동이나 시점 제한도 짝을 맞춰 풀어야 합니다. 그리고 입력 모드를 바꾼다고 월드가 일시 정지되거나 서버의 규칙이 검증되지는 않으므로, 필요하면 그 처리는 따로 합니다.
정리
- UI와 게임플레이는 플레이어 입력을 게임에 전달하는 방향과, 게임 상태를 화면에 반영하는 방향으로 연결됩니다.
- 이벤트 디스패처는 UI가 알리기만 하고 받는 쪽이 등록해 반응하므로 결합이 약하고, 직접 함수 호출은 간단하지만 위젯이 특정 블루프린트에 의존하게 됩니다.
Expose on Spawn을 켠 변수는Create Widget노드의 입력 핀으로 나타나, 위젯을 만들 때 참조를 넘길 수 있습니다.- 속성 바인딩은 값을 반복해서 읽으므로, 비율 계산에서는 0으로 나누지 않게 하고 한 속성을 바인딩과 직접 갱신이 함께 덮어쓰지 않게 합니다.
- 입력 모드는 입력이 UI와 게임 중 어디로 갈지 정하며, UI가 소비한 입력은 게임으로 다시 전달되지 않고 커서와 일시 정지는 따로 설정합니다.
연습 문제
1. 같은 키를 누가 처리할까
Game And UI 모드에서 포커스된 텍스트 입력창이 I 키를 처리해 소비했습니다. 인벤토리를 닫는 처리를 컨트롤러의 I 키 이벤트에만 연결해 두었다면, 이 키 입력이 반드시 그 이벤트까지 전달될까요? 창을 닫을 수 있게 하려면 무엇을 먼저 점검해야 할까요?
해설 보기
전달된다고 볼 수 없습니다. Game And UI에서도 UI가 소비한 입력은 게임 쪽으로 다시 전달되지 않으므로, 텍스트 입력창이 I 키를 처리했다면 컨트롤러의 I 키 이벤트는 실행되지 않을 수 있습니다.
먼저 UI에 닫기 버튼이나 필요한 키 처리 경로를 마련하고, 그 경로에서 Remove from Parent, 게임 입력 모드, 커서, 짝을 맞춘 이동과 시점 제한의 복구를 모두 호출하는지 확인합니다. 게임 쪽 키 이벤트를 더 만드는 것보다, 지금 포커스가 어디에 있고 UI가 어떤 키를 처리하는지를 먼저 조사하는 편이 원인을 빨리 찾습니다.
2. 체력 바의 비율 계산
CurrentHealth가 30, MaxHealth가 120일 때 바인딩 함수가 돌려주는 Percent는 얼마일까요? 회복 아이템 때문에 CurrentHealth가 150이 되었다면 어떻게 될까요? MaxHealth가 아직 설정되지 않아 0이라면요?
해설 보기
30 / 120 = 0.25이므로 체력 바가 4분의 1만큼 채워집니다.
CurrentHealth가 150이면 150 / 120 = 1.25가 되지만, 본문의 함수는 결과를 0에서 1 사이로 Clamp하므로 1.0을 돌려주고 체력 바는 가득 찬 상태로 표시됩니다. MaxHealth가 0이면 나눗셈을 하지 않는 경로로 가서 기본값 0을 돌려줍니다. 체력이 최대치를 넘는 것 자체가 버그일 수 있으므로, 회복 로직에서도 CurrentHealth를 MaxHealth 이하로 제한하는 편이 좋습니다.
3. 연결 방식 고르기
다음 두 기능을 이벤트 디스패처와 직접 함수 호출 가운데 무엇으로 연결하는 것이 알맞을지 이유와 함께 골라 보세요. (가) 일시 정지 메뉴의 "계속하기" 버튼을 누르면 플레이어 컨트롤러, 배경 음악 관리자, 타이머 관리자가 모두 반응해야 합니다. (나) 플레이어 캐릭터 전용 장비 창에서 장비 버튼을 누르면 그 캐릭터의 EquipItem 함수 하나만 호출하면 됩니다.
해설 보기
(가)는 이벤트 디스패처가 알맞습니다. 버튼 위젯은 "계속하기가 눌렸다"고 한 번 알리기만 하고, 세 블루프린트가 각자 등록해 반응하면 됩니다. 반응할 곳이 늘어나도 위젯을 고칠 필요가 없습니다.
(나)는 직접 함수 호출로도 충분합니다. 장비 창이 그 캐릭터 전용이고 호출할 함수가 하나뿐이라면, Expose on Spawn으로 캐릭터 참조를 받아 EquipItem을 부르는 편이 설정이 간단합니다. 다만 나중에 다른 캐릭터도 이 창을 써야 한다면 디스패처나 인터페이스로 바꾸는 것을 검토합니다.
위젯 속성에 함수나 변수를 바인딩하는 방법은 언리얼 엔진 공식 문서의 UMG 속성 바인딩에서 확인할 수 있습니다.
다음 글 간단한 인벤토리 시스템 구현에서는 지금까지 배운 위젯, 동적 UI, 게임플레이 연결을 모아 아이템을 줍고 표시하고 사용하는 간단한 인벤토리 시스템을 만듭니다.