간단한 인벤토리 시스템 구현
아이템 구조체·획득 액터·플레이어 배열·슬롯 위젯을 연결해 동적으로 갱신되는 인벤토리를 완성합니다.
이번 장에서 위젯 블루프린트를 만들고, 게임 중에 위젯을 생성하고, 애니메이션을 넣고, UI와 게임플레이를 연결하는 방법을 하나씩 익혔습니다. 하지만 기능을 따로 배울 때와 하나의 시스템으로 묶을 때는 다릅니다. 아이템을 주웠는데 창에 나타나지 않거나, 창을 닫았더니 아이템이 사라지거나, 창을 닫은 뒤 캐릭터가 움직이지 않는 문제는 대개 여러 블루프린트 사이의 연결에서 생깁니다.
이 글에서는 지금까지의 내용을 모아 간단한 인벤토리 시스템을 만듭니다. 인벤토리는 UI와 게임플레이 로직이 밀접하게 이어지는 좋은 예로, 5장의 구조체와 데이터 관리, 4장의 블루프린트 통신, 이번 장의 위젯 기법이 함께 쓰입니다. 무엇보다 아이템 데이터를 가진 쪽과 그것을 보여 주는 쪽을 분명히 나누는 데 집중합니다.
만들 인벤토리와 준비할 블루프린트
완성할 인벤토리는 네 가지 일을 합니다. 플레이어가 아이템을 획득하면 인벤토리에 추가되고, 인벤토리 창에 획득한 아이템이 동적으로 표시되며, 각 칸에는 아이템의 아이콘과 이름이 나타납니다. 그리고 키를 눌러 인벤토리 창을 열고 닫을 수 있습니다.
이를 위해 다음 블루프린트가 필요합니다.
| 블루프린트 | 하는 일 |
|---|---|
F_ItemData 구조체 | 아이템 한 개의 데이터. 5장의 구조체에 ItemIcon이 아직 없다면 추가해 ItemName(Text), ItemIcon(Texture 2D Soft Object Reference), Description(Text), AttackPower(Integer), Weight(Float)를 담습니다. 기존 ItemMesh 필드는 필요하면 그대로 둡니다. |
BP_PickupItem 액터 | 월드에 놓여 있다가, 플레이어와 겹치면 자기 F_ItemData를 플레이어에게 넘깁니다. |
BP_PlayerCharacter | 아이템 데이터를 PlayerInventory(F_ItemData의 배열)에 보관하고, 화면 갱신을 요청합니다. |
WBP_InventorySlot 위젯 | 아이템 한 개의 아이콘과 이름을 표시하고, 데이터를 받아 스스로 표시를 바꾸는 함수를 가집니다. |
WBP_InventoryScreen 위젯 | 슬롯을 동적으로 추가할 Grid Panel을 가지고, 인벤토리 배열을 받아 슬롯을 새로 그리는 함수를 가집니다. |
BP_PlayerController | 인벤토리 화면을 만들어 참조를 보관하고, 키 입력으로 창을 열고 닫습니다. |
데이터와 화면의 흐름
아이템 데이터에서 화면 슬롯까지
픽업 ItemInfo→캐릭터 배열 갱신→유효 화면 요청→슬롯 재구성의 실제 상태 변화이다. 화면이 없으면 데이터는 유지하고 표시만 생략한다.
MODEL DATA → SCREEN → SLOTS
아이템은 배열에 보관하고 위젯은 그 값을 표시합니다
픽업이 ItemInfo를 전달하면 캐릭터가 PlayerInventory 배열을 갱신합니다. 컨트롤러의 InventoryScreenRef가 유효하면 현재 배열로 화면과 슬롯을 갱신합니다.
Clear Children은 화면의 자식 슬롯을 제거합니다. 플레이어의 인벤토리 배열을 지우는 작업은 아닙니다.
화면이 아직 없어도 데이터는 유지합니다
위젯이 없으면 표시 갱신만 건너뜁니다. 나중에 창을 열 때 최신 배열을 전달하며, 같은 픽업은 Do Once 뒤 한 번만 추가하는 예제입니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 픽업이 ItemInfo를 전달하면 캐릭터 배열에 추가한다. 유효한 화면 참조가 있을 때만 화면 갱신을 호출하고, 그 화면은 자식 슬롯을 만든다. UI가 없어도 배열은 유지한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 픽업 데이터 ItemInfo를 캐릭터 인벤토리 배열에 반영한다. |
|
|
| 화면 준비 유효한 인벤토리 화면을 찾는다. |
|
|
| 슬롯 재구성 현재 배열을 기준으로 슬롯을 갱신한다. |
|
|
아이템 데이터에서 화면 슬롯까지
픽업 ItemInfo→캐릭터 배열 갱신→유효 화면 요청→슬롯 재구성의 실제 상태 변화이다. 화면이 없으면 데이터는 유지하고 표시만 생략한다.
MODEL DATA → SCREEN → SLOTS
아이템은 배열에 보관하고 위젯은 그 값을 표시합니다
픽업이 ItemInfo를 전달하면 캐릭터가 PlayerInventory 배열을 갱신합니다. 컨트롤러의 InventoryScreenRef가 유효하면 현재 배열로 화면과 슬롯을 갱신합니다.
Clear Children은 화면의 자식 슬롯을 제거합니다. 플레이어의 인벤토리 배열을 지우는 작업은 아닙니다.
화면이 아직 없어도 데이터는 유지합니다
위젯이 없으면 표시 갱신만 건너뜁니다. 나중에 창을 열 때 최신 배열을 전달하며, 같은 픽업은 Do Once 뒤 한 번만 추가하는 예제입니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 픽업이 ItemInfo를 전달하면 캐릭터 배열에 추가한다. 유효한 화면 참조가 있을 때만 화면 갱신을 호출하고, 그 화면은 자식 슬롯을 만든다. UI가 없어도 배열은 유지한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 픽업 데이터 ItemInfo를 캐릭터 인벤토리 배열에 반영한다. |
|
|
| 화면 준비 유효한 인벤토리 화면을 찾는다. |
|
|
| 슬롯 재구성 현재 배열을 기준으로 슬롯을 갱신한다. |
|
|
위 그림은 아이템 데이터가 화면의 슬롯에 이르는 경로를 보여 줍니다. 픽업 액터가 ItemInfo를 넘기면 캐릭터가 인벤토리 배열에 추가합니다. 컨트롤러가 보관한 화면 참조(InventoryScreenRef)가 유효할 때만 캐릭터가 화면 갱신을 요청하고, 인벤토리 화면은 그 배열로 자식 슬롯을 다시 만들어 각 슬롯에 이름과 아이콘을 표시합니다. 그림이 강조하는 것은 아이템은 배열이 보관하고 위젯은 그 값을 표시할 뿐이라는 역할의 구분입니다.
단계 그림은 이 경로를 세 단계로 나눕니다. 먼저 픽업 데이터인 ItemInfo를 캐릭터의 인벤토리 배열에 반영하고, 다음으로 유효한 인벤토리 화면이 있는지 확인한 뒤, 마지막으로 현재 배열을 기준으로 슬롯을 다시 구성합니다. 그림 아래의 두 설명처럼 Clear Children은 화면의 자식 슬롯을 지울 뿐 플레이어의 인벤토리 배열을 지우지 않고, 화면이 아직 없으면 표시 갱신만 건너뛰고 데이터는 그대로 유지해 나중에 창을 열 때 최신 배열을 넘깁니다. 같은 픽업은 Do Once를 거쳐 한 번만 추가되도록 만듭니다.
주울 수 있는 아이템 BP_PickupItem
먼저 월드에 놓여 있다가 플레이어가 주울 수 있는 아이템을 만듭니다. Actor를 부모로 BP_PickupItem을 만들고, 모양을 보여 줄 Static Mesh 컴포넌트(예: Shape_Sphere)와 플레이어를 감지할 Sphere Collision 컴포넌트를 추가합니다. Sphere Collision은 플레이어와 Query 및 Overlap으로 반응하도록 콜리전 응답을 설정하고, 겹침 이벤트가 일어나도록 이 컴포넌트와 플레이어 쪽 컴포넌트 모두 Generate Overlap Events를 켭니다.
다음으로 ItemInfo 변수(타입: F_ItemData)를 추가하고, 디테일 패널에서 Instance Editable과 Expose on Spawn을 켭니다. 이렇게 하면 월드에 배치한 BP_PickupItem 인스턴스마다 다른 아이템 정보를 입력할 수 있습니다.
이벤트 그래프에서는 Sphere Collision의 On Component Begin Overlap 이벤트를 출발점으로 삼습니다. 겹친 Other Actor를 Cast To BP_PlayerCharacter로 캐스트해 플레이어인지 확인하고, 성공했다면 Do Once를 지나게 해 같은 픽업이 두 번 처리되지 않도록 한 뒤, 플레이어의 AddItemToInventory에 ItemInfo를 넘깁니다. 이 단순한 예제는 인벤토리 용량에 제한이 없어 배열에 언제나 추가할 수 있다고 가정합니다. 아이템 추가 호출이 끝난 흐름에서만 Destroy Actor로 픽업 자신을 제거하며, 이때 UI가 아직 없더라도 인벤토리 데이터의 추가는 그대로 유지됩니다.
컴파일하고 저장한 뒤 메인 에디터로 돌아와 BP_PickupItem 인스턴스를 월드에 여러 개 배치하고, 각 인스턴스의 ItemInfo에 다른 값을 입력해 봅니다(예: ItemName은 "골드", ItemIcon에는 골드 아이콘 텍스처).
아이템을 보관하는 BP_PlayerCharacter
플레이어 캐릭터는 아이템 데이터를 보관하고 화면 갱신을 요청하는 역할을 합니다. PlayerInventory 변수(타입: F_ItemData의 배열)를 추가하고, F_ItemData 타입의 NewItem을 입력으로 받는 AddItemToInventory 함수를 만듭니다.
함수 안에서는 먼저 PlayerInventory에서 끌어 Add 노드를 호출하고 NewItem을 연결합니다. 원한다면 Print String으로 "아이템 [NewItem.ItemName] 획득!" 같은 메시지를 출력합니다. 그다음 캐릭터를 조종하는 컨트롤러를 BP_PlayerController로 캐스트해, 뒤에서 저장할 InventoryScreenRef를 읽습니다. 참조가 유효하면 RefreshInventoryUI에 현재 PlayerInventory를 넘기고, 아직 위젯이 없으면 이 UI 호출만 건너뜁니다. 나중에 창을 열 때 최신 배열로 갱신하므로 데이터는 잃지 않습니다.
아이템 한 칸을 그리는 WBP_InventorySlot
User Widget을 부모로 WBP_InventorySlot을 만듭니다. 디자이너 탭에서는 Size Box를 배치하고 Width Override와 Height Override를 100x100 정도로 정해 슬롯 크기를 고정합니다. 그 자식으로 배경색을 줄 Border를, 그 안에 Vertical Box를 놓고, Vertical Box의 자식으로 Image(이름: ItemIcon)와 Text(이름: ItemNameText)를 배치해 둘 다 Is Variable을 켭니다. 아이콘과 이름이 잘 보이도록 정렬과 여백을 조절합니다.
그래프 탭에서는 ItemData(타입: F_ItemData)를 입력으로 받는 UpdateSlot 함수를 만듭니다. 입력에서 Break F_ItemData로 멤버를 꺼낸 뒤, ItemNameText 위젯 참조의 Get 노드에서 Set Text를 호출해 ItemName을 연결합니다. 여기서 쓰는 것은 텍스트 내용을 바꾸는 Set Text이며, 위젯 참조 변수 자체를 다른 객체로 바꾸는 변수 Set 노드와는 다릅니다.
아이콘은 ItemIcon 위젯 참조에서 Set Brush from Soft Texture를 호출하고, 구조체의 ItemIcon 경로를 Soft Texture 핀에 연결합니다. 이 노드가 소프트 텍스처를 불러오는 요청까지 처리합니다. 반면 Make Brush from Texture는 이미 로드된 Texture2D를 받는 노드이므로, 소프트 참조가 이 핀에서 저절로 로드된다고 가정하면 안 됩니다. 직접 비동기 로드를 제어해야 한다면 함수 밖의 이벤트 그래프에서 Async Load Asset을 호출하고, 완료된 뒤 유효한 텍스처를 적용합니다.
슬롯을 모아 그리는 WBP_InventoryScreen
User Widget을 부모로 WBP_InventoryScreen을 만듭니다. 디자이너 탭에서는 Canvas Panel에 Border를 배치해 인벤토리 창의 배경을 만들고 크기와 색을 조절합니다. Border의 자식으로 Grid Panel을 놓고 이름을 InventoryGrid로 정해 Is Variable을 켭니다. 그리드 전체의 공간 배분은 Grid Panel의 Column Fill과 Row Fill로 정하고, 각 자식이 들어갈 칸은 자식의 Grid Slot에서 행과 열로 정합니다.
그래프 탭에서는 CurrentInventory(타입: F_ItemData의 배열)를 입력으로 받는 RefreshInventoryUI 함수를 만듭니다. 먼저 InventoryGrid의 Get 노드에서 Clear Children을 호출해 기존 슬롯을 모두 지우고 새로 그릴 준비를 합니다. 그다음 CurrentInventory를 For Each Loop에 연결하고, Loop Body 뒤에서 아이템마다 다음 일을 합니다.
Create Widget의 Class에 WBP_InventorySlot을 고르고 Owning Player에는 Get Owning Player를 연결해 슬롯을 만듭니다. Create Widget의 Return Value는 고른 클래스 타입으로 나오므로, 캐스트 없이 이 핀에서 바로 UpdateSlot을 호출해 For Each Loop의 Array Element를 연결합니다. 이어서 InventoryGrid에서 Add Child to Grid를 호출해 Target에 InventoryGrid를, Content에 같은 Return Value를 연결하고, 행과 열은 For Each Loop의 Index로 계산합니다. 5열 그리드라면 열은 Index % 5, 행은 Index / 5입니다.
창을 열고 닫는 BP_PlayerController
인벤토리 창은 플레이어 컨트롤러가 관리합니다. BP_PlayerController에 InventoryScreenRef 변수(타입: WBP_InventoryScreen Object Reference)를 추가합니다. Event BeginPlay 뒤에서 Create Widget으로 WBP_InventoryScreen을 만들고, Owning Player에는 컨트롤러 자신(Self)을 연결한 뒤, Return Value를 Set InventoryScreenRef로 저장합니다. 이 시점에는 화면에 추가하지 않습니다.
이 예제는 로컬 플레이어 한 명을 대상으로 합니다. 컨트롤러에 키보드 I 키 이벤트나 프로젝트에서 쓰는 대응 Input Action 이벤트를 연결하고, 실제 UI에서는 게임패드와 키보드 포커스, 닫기 버튼도 함께 확인합니다. 키 이벤트에서는 먼저 InventoryScreenRef를 Is Valid로 검사하고, 유효하면 Is In Viewport로 화면에 추가되어 있는지 확인해 Branch로 나눕니다. Is Visible은 Visibility 속성을 기준으로 하므로, 화면에 추가되었는지를 대신 판단하는 용도로 쓰지 않습니다.
Is In Viewport가 True이면 창이 열려 있으므로 닫습니다. InventoryScreenRef에서 Remove from Parent를 호출해 화면에서 떼어 내고, Set Input Mode Game Only로 입력을 게임에 돌린 뒤, Set Show Mouse Cursor로 커서를 숨깁니다. 창을 열 때 이동과 시점 입력을 막았다면 Set Ignore Move Input과 Set Ignore Look Input에 false를 넘겨 짝을 맞춰 풉니다.
False이면 창이 닫혀 있으므로 엽니다. InventoryScreenRef에서 Add to Viewport를 호출하고 Set Input Mode Game And UI를 설정합니다. 이 예제의 위젯은 I 키를 처리하지 않으므로, Game And UI에서도 컨트롤러가 닫기 입력을 받을 수 있습니다. UI Only를 쓰려면 위젯 안에 따로 닫기 입력 경로를 만들어야 합니다. 이어서 Set Show Mouse Cursor로 커서를 보이게 하고, 창이 열린 동안 이동과 시점을 막으려면 이 열기 분기에서만 Set Ignore Move Input과 Set Ignore Look Input에 true를 한 번씩 넘깁니다. 마지막으로 InventoryScreenRef의 RefreshInventoryUI를 호출해, 조종 중인 BP_PlayerCharacter의 PlayerInventory를 연결합니다. 이렇게 하면 창을 열 때마다 캐릭터의 최신 인벤토리가 화면에 그려집니다.
동작 확인하기
테스트하기 전에 아이템 획득, 배열 갱신, 슬롯 생성, 입력 모드 복구가 한 흐름 안에서 이어지는지 다시 확인하고, 모든 블루프린트를 컴파일하고 저장합니다. 게임 모드에는 BP_PlayerController와 플레이어 캐릭터를 지정하고, 로컬 플레이어가 그 캐릭터를 조종하도록 구성합니다. 이때 게임 모드가 캐릭터를 스폰하게 할지, 레벨에 배치한 폰을 빙의(Possess)하게 할지 한 가지 방식만 고릅니다. 그리고 BP_PickupItem을 여러 개 배치합니다.
플레이해서 BP_PickupItem에 겹치면 아이템을 획득하고 메시지가 출력되는지 봅니다. I 키를 누르면 인벤토리 창이 나타나고 획득한 아이템들이 그리드에 표시되는지, I 키를 다시 누르면 창이 닫히고 마우스 커서가 사라지며 캐릭터를 다시 조작할 수 있는지 확인합니다. 아이템을 한 번 획득할 때마다 배열 요소가 정확히 하나 늘어나는지, 창을 열고 닫을 때 입력 상태도 함께 돌아오는지가 핵심입니다.
이 인벤토리는 기본형입니다. 여기에 아이템 사용, 버리기, 드래그 앤 드롭, 툴팁, 같은 아이템을 한 칸에 쌓는 스택 같은 기능을 더해 갈 수 있습니다. 어느 기능을 더하든 데이터는 캐릭터의 배열이 가지고, 화면은 그 배열을 받아 다시 그린다는 구분을 유지하면 연결이 꼬이지 않습니다.
정리
- 인벤토리 시스템은 픽업 액터, 데이터를 보관하는 캐릭터, 화면을 관리하는 컨트롤러, 화면 위젯과 슬롯 위젯으로 역할을 나눕니다.
- 아이템은 캐릭터의
PlayerInventory배열이 보관하고, 위젯은 그 배열을 받아 표시만 합니다. 화면이 없을 때는 표시 갱신만 건너뜁니다. - 픽업은 캐스트 성공 뒤
Do Once로 한 번만 처리하고, 아이템 추가가 끝난 뒤에 자신을 제거합니다. - 소프트 아이콘은
Set Brush from Soft Texture로 넘기고,Create Widget의 반환 값은 캐스트 없이 슬롯의 함수를 호출하는 데 씁니다. - 창을 열고 닫을 때는
Is In Viewport로 상태를 확인하고, 입력 모드와 커서, 이동과 시점 제한을 짝을 맞춰 설정하고 되돌립니다.
연습 문제
1. 화면을 만들기 전에 주운 아이템
인벤토리 배열이 비어 있고 UI 참조도 아직 없는 상태에서, 서로 다른 픽업 2개를 획득했습니다. 각 픽업은 플레이어 캐스트 성공, Do Once, 배열 추가, 자기 제거를 한 번씩 실행했고 용량 제한은 없습니다. 그 뒤 화면을 만들고 최신 배열로 RefreshInventoryUI를 한 번 호출하면 슬롯은 몇 개여야 할까요? 또 화면을 제거해 창을 닫았다면 다음에 열 때 아이템이 사라져야 할까요?
해설 보기
배열 요소는 2개이므로 슬롯도 2개여야 합니다. 처음에 UI 참조가 없어 표시 갱신을 건너뛰었다고 배열 추가까지 취소되지는 않습니다.
Remove from Parent는 화면을 떼어 낼 뿐 캐릭터의 배열을 비우지 않으므로, 다음에 열 때도 최신 2개가 전달됩니다. 다만 이 예제는 같은 아이템 합치기, 용량 제한, 멀티플레이 동기화를 구현하지 않았으므로, 그런 기능이 있는 인벤토리에도 이 결과가 그대로 맞는다고 일반화하지는 않습니다.
2. 창을 닫았는데 캐릭터가 움직이지 않음
인벤토리 창을 열 때 Set Ignore Move Input에 true를 넘기는 처리를 열기 분기와 RefreshInventoryUI 안에 각각 한 번씩 넣었습니다. 닫기 분기에서는 false를 한 번 넘깁니다. 창을 닫아도 캐릭터가 움직이지 않는 이유는 무엇일까요?
해설 보기
Set Ignore Move Input은 켜고 끄는 스위치가 아니라, true를 넘긴 횟수만큼 false를 넘겨야 풀리는 방식으로 동작합니다. 창을 한 번 열 때 true가 두 번 들어갔는데 닫을 때 false는 한 번만 들어가므로, 이동 무시가 한 번 남아 있습니다.
true는 열기 분기에서만 한 번 넘기고, RefreshInventoryUI처럼 여러 번 호출될 수 있는 함수에서는 빼야 합니다. 그러면 열 때 한 번, 닫을 때 한 번으로 짝이 맞습니다. 시점 입력의 Set Ignore Look Input도 같은 방식으로 짝을 맞춥니다.
3. 숨기기만 한 픽업
픽업을 다시 쓰려고 Destroy Actor 대신 Set Actor Hidden In Game으로 숨기기만 하도록 바꾸고, Do Once도 지웠습니다. 플레이어가 숨겨진 픽업의 구체 밖으로 나갔다가 다시 들어오면 인벤토리 배열에는 어떤 일이 생길까요? 본문의 구성은 이를 어떻게 막고 있었을까요?
해설 보기
같은 아이템이 또 추가됩니다. Set Actor Hidden In Game은 액터를 화면에 그리지 않게 할 뿐 콜리전은 그대로 두므로, 플레이어가 구체에 다시 들어오면 On Component Begin Overlap이 다시 실행되고 AddItemToInventory도 다시 호출됩니다. 화면에서 사라졌다는 것과 더 이상 상호작용하지 않는다는 것은 다른 상태입니다.
본문은 캐스트 성공 뒤의 Do Once로 같은 픽업이 한 번만 처리되게 하고, 아이템을 추가한 뒤 Destroy Actor로 픽업 자체를 없앴습니다. 숨기는 방식을 쓰려면 Do Once를 유지하거나, 숨길 때 Set Actor Enable Collision으로 콜리전도 함께 꺼야 합니다.
위젯을 만들어 화면에 추가하고 Remove from Parent로 화면에서 떼어 내는 흐름은 언리얼 엔진 공식 문서의 위젯 생성에서 확인할 수 있습니다.
이것으로 6장을 마칩니다. 6장에서는 위젯 블루프린트로 버튼을 만들어 화면에 띄우는 것에서 시작해, 게임 중에 위젯을 만들어 패널에 채우는 동적 UI, 위젯 애니메이션, 이벤트 디스패처와 바인딩과 입력 모드로 UI와 게임플레이를 잇는 방법을 익히고, 이를 모아 간단한 인벤토리를 만들었습니다. 다음 장의 애님 그래프의 기본 구성부터는 애니메이션 블루프린트로 캐릭터의 움직임을 다루며, 애님 그래프와 스테이트 머신, 블렌드 스페이스, 애님 몽타주와 노티파이를 차례로 살펴봅니다.