파밍과 크래프팅, 그리고 열쇠를 통한 탈출까지. 이 모든 시스템이 맞물려 돌아가려면 아이템을 담고 관리할 튼튼한 '가방'이 필요하다. 단순히 아이템을 배열에 넣는 것을 넘어, 생존 호러의 템포를 해치지 않는 모듈화 된 UOblivioInventoryComponent를 개발했다.
1. 인벤토리 데이터 구조(FInventorySlot) 설계
아이템의 정보가 파편화되지 않도록 FInventorySlot이라는 구조체(Struct)를 정의하여 하나의 슬롯이 독립적인 데이터를 갖도록 만들었다.
- 슬롯 안에는 ItemID, ItemName, ItemIcon, MaxStack(최대 겹침 수량), ItemType(음식, 자원, 열쇠 등), 그리고 현재 Quantity를 담았다.
- 구조체를 활용하니 인벤토리 UI를 띄울 때 해당 슬롯의 데이터만 쏙 빼서 위젯에 바인딩하기가 매우 수월해졌다.
2. 핵심 인벤토리 기능 및 최적화 로직
아이템을 획득(AddItem)할 때의 알고리즘을 현실적으로 짰다.
- 스태킹(Stacking) 우선: 아이템을 주웠을 때 무조건 빈칸에 넣는 것이 아니라, FindStackableSlot()을 먼저 호출하여 겹칠 수 있는 슬롯이 있는지 확인한다.
- 분산 소모 로직: 크래프팅 시 자원을 소모하는 ConsumeItem() 함수는 꽤 까다로웠다. 나무 10개가 필요한데 슬롯 A에 6개, 슬롯 B에 4개가 있을 수 있기 때문이다. 남은 차감량(RemainingToConsume)을 계산하여 여러 슬롯을 순회하며 정확히 자원을 빼고, 0개가 된 슬롯은 깔끔하게 빈 슬롯으로 초기화(FInventorySlot())하도록 로직을 구현했다.
3. 게임 인스턴스(GameInstance) 동기화
어제 만든 층간 이동 시스템과 결합했다. 맵이 바뀌어도 가방 안의 아이템이 날아가지 않도록, 인벤토리에 변화가 생길 때마다 SyncInventoryToGameInstance()를 호출했다.
- 배열 통째로 GI->SavedInventorySlots에 덮어씌워 보존하고, 다음 층이 로드될 때 BeginPlay에서 그대로 가져와 인벤토리를 복원한다.
코드:
1. 아이템 획득 및 스태킹(중첩) 로직 (OblivioInventoryComponent.cpp)
bool UOblivioInventoryComponent::AddItem(AOblivioItemBase* Item)
{
if (!Item) return false;
// 1. 기존에 겹칠 수 있는 동일한 아이템 슬롯 탐색
int32 TargetSlot = FindStackableSlot(Item->ItemID);
// 2. 겹칠 곳이 없으면 새로운 빈 공간 탐색
if (TargetSlot == INDEX_NONE) TargetSlot = FindEmptySlot();
if (TargetSlot != INDEX_NONE)
{
FInventorySlot& Slot = InventorySlots[TargetSlot];
// 빈 슬롯이면 새 데이터 삽입, 아니면 수량만 추가
if (Slot.IsEmpty())
{
Slot.ItemID = Item->ItemID;
Slot.ItemType = Item->ItemType;
Slot.MaxStack = Item->MaxQuantity;
Slot.Quantity = Item->CurrentQuantity;
// ... (기타 데이터 생략) ...
}
else
{
Slot.Quantity = FMath::Clamp(Slot.Quantity + Item->CurrentQuantity, 0, Slot.MaxStack);
}
SyncInventoryToGameInstance(); // 획득 즉시 인스턴스와 동기화 (저장)
return true;
}
return false;
}
2. 크래프팅 시 분산된 자원 소모 로직 (ConsumeItem)
bool UOblivioInventoryComponent::ConsumeItem(EItemType Type, int32 Amount)
{
if (GetItemCount(Type) < Amount) return false;
int32 RemainingToConsume = Amount;
for (FInventorySlot& Slot : InventorySlots)
{
if (!Slot.IsEmpty() && Slot.ItemType == Type)
{
if (Slot.Quantity >= RemainingToConsume)
{
// 현재 슬롯만으로 충분히 차감 가능한 경우
Slot.Quantity -= RemainingToConsume;
RemainingToConsume = 0;
if (Slot.Quantity <= 0) Slot = FInventorySlot(); // 슬롯 초기화
break;
}
else
{
// 현재 슬롯을 모두 비우고 다음 슬롯으로 넘어가서 마저 차감
RemainingToConsume -= Slot.Quantity;
Slot = FInventorySlot();
}
}
}
SyncInventoryToGameInstance();
return true;
}
인벤토리는 게임 프로그래밍의 꽃이자 '배열(Array)'과 '구조체(Struct)'를 다루는 끝판왕이라는 말을 실감했다. 특히 크래프팅 시스템과 연동하면서, 특정 타입(EItemType::Wood 등)의 총합을 구하고 차감하는 로직을 분리해 두니 이후 디버깅과 확장이 압도적으로 편해졌다.