카테고리 없음

Oblivio 인벤토리 시스템 개발: 생존의 무게를 담다

eclipse2 2026. 5. 13. 00:15

파밍과 크래프팅, 그리고 열쇠를 통한 탈출까지. 이 모든 시스템이 맞물려 돌아가려면 아이템을 담고 관리할 튼튼한 '가방'이 필요하다. 단순히 아이템을 배열에 넣는 것을 넘어, 생존 호러의 템포를 해치지 않는 모듈화 된 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 등)의 총합을 구하고 차감하는 로직을 분리해 두니 이후 디버깅과 확장이 압도적으로 편해졌다.