LeeTaes 공부노트

[UE Team Project/T.A.] 7. 아이템 및 인벤토리 - 1 (Component) 본문

프로젝트/TimelessAdventure

[UE Team Project/T.A.] 7. 아이템 및 인벤토리 - 1 (Component)

리태s 2024. 10. 10. 10:03
728x90
반응형

개요

이번 포스팅에서는 아이템과 인벤토리를 구현하는 방법에 대해 정리해보도록 하겠습니다.

 

생각보다 다룰 내용이 많아 구현 아이디어와 실제 Inventory Component의 코드를 리뷰하고 UI와 관련된 부분은 다음 포스팅에서 진행하도록 하겠습니다.


구현 아이디어 설명

저는 이전에 아이템과 인벤토리를 데이터 애셋을 활용해 제작해본 경험이 있습니다. 하지만 이번에는 데이터 테이블을 사용해보자는 의견이 있었고, 해당 의견에 맞춰 구현을 진행하게 되었습니다.

 

우선 아이템의 정보를 저장하기 위한 클래스가 필요합니다.

 

아이템의 이름, 아이템의 아이콘, 최대 수량 등 아이템 정보를 관리하기 위한 구조체가 필요하며, 실제 인벤토리에서는 아이템 액터를 들고있는 것이 아닌 아이템 정보인 구조체만 TArray<아이템 구조체>로 관리할 예정입니다.

 

원래는 TMap<>을 사용해 아이템 탐색에 이점을 주는 방식으로 공부했지만, 저희 팀이 기획한 인벤토리는 드래그&드랍 기능을 추가하여 아이템의 위치를 변환해야 하므로 TMap보다는 임의 인덱스에 접근 가능한 TArray로 구현하게 되었습니다.

 

다음으로는 실제 인벤토리와 연동될 UI에 대한 아이디어 입니다.

 

UI 상의 각 Slot은 개별적으로 인덱스와 타입을 가지고 있어야 하며, 해당 인덱스와 타입을 통해 실제 인벤토리에 저장된 아이템 데이터와 연동되어야 합니다.

 

즉, Slot은 타입과 인덱스, 그리고 자신의 부모 클래스에 대한 정보를 저장해야 하며 대다수의 UI에서 부모의 정보를 필요로 할 것이라고 생각하여 CustomWidget을 만들고 이를 상속받아 모든 UI를 저장할 예정입니다.


아이템 정보 구조체 및 데이터 테이블 생성 

아이템은 포션(Hp, Stamina)와 퀘스트 아이템 3종으로 총 5가지만 게임에 등장할 예정입니다.

 

즉, 데이터 테이블을 따로 만들지 않고 간단히 하나로 합쳐서 만들어도 되는 수준이며 그렇기에 아이템 정보 구조체에서 모든 아이템의 정보를 저장할 수 있도록 만들어주었습니다. 

 

FItemData.h

더보기
#pragma once

#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "TA_ItemData.generated.h"

UENUM(BlueprintType)
enum class EItemType : uint8
{
    Consumable,     // 소비
    Miscellaneous,  // 기타
};

USTRUCT(BlueprintType)
struct FItemData : public FTableRowBase
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    int32 ItemID;

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    EItemType EItemType;

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    FText ItemName;

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    FText ItemDescription;

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    UTexture2D* ItemThumbnail;

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    int32 Price;

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    bool bIsStackable;

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    int32 MaxStackCount;

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    float AddHPAmount;

    UPROPERTY(EditAnywhere, Category = "Item", BlueprintReadWrite)
    float AddStaminaAmount;
};

 

위 구조체를 사용해 에디터 상에서 데이터 테이블을 제작해주도록 합니다.

아이템 데이터 테이블


인벤토리 제작 - 인벤토리 아이템 

위에서 생성한 아이템 정보를 저장하는 인벤토리를 제작할 것입니다.

 

단순히 아이템의 정보만을 저장하기에는 부족한 정보들이 많기에 인벤토리에 아이템을 저장하기 위한 구조체를 선언하였으며, 해당 구조체를 인벤토리에 저장하도록 하였습니다.

인벤토리에 들어갈 아이템 구조체

또한, 제가 구상했던 인벤토리는 소비, 기타 슬롯으로 구분되어 있기에 해당 정보를 저장할 별도의 배열과 해당 슬롯의 타입을 구분하기 위한 열거형을 선언하였습니다.

소비(C), 기타(M), 퀵슬롯 데이터 저장용 TArray
슬롯(UI)의 타입을 구분하기 위한 열거형

 

인벤토리는 아이템을 추가, 삭제, 소모, 사용, 구매 하는 기능이 필요합니다. 각 기능에 대해 하나씩 정리해보도록 하겠습니다.


아이템 추가 기능

  • 아이템 이름을 통해 아이템을 추가하는 기능입니다.
  • 타입별로 저장되는 인벤토리 (소비, 기타)가 달라져야 합니다.
  • 이미 해당 타입의 아이템이 존재하고, 중첩 가능하다면 수량을 올려줘야 합니다.
  • 최대 수량을 전부 채웠을 경우 다음 빈 칸에 아이템을 추가해야 합니다.
  • 인벤토리가 가득 차서 아이템을 저장하지 못하는 경우 false를 리턴하도록 bool 타입으로 제작하였습니다.

추가적으로 인벤토리 내부의 정보가 변동될 때마다 UI를 업데이트 해주기 위해 Delegate를 선언하였습니다.

더보기
bool UTA_InventoryComponent::AddItem(FName ItemName, int32& Quantity)
{
    // 해당 이름의 아이템 데이터 가져오기
    UTA_GameInstance* GI = Cast<UTA_GameInstance>(GetWorld()->GetGameInstance());
    if (!GI) return false;

    FItemData ItemData = GI->GetItemData(ItemName);

    // 인벤토리 아이템 생성
    FInvItem NewItem;
    NewItem.Data = ItemData;
    NewItem.Quantity = Quantity;
    NewItem.bIsEmpty = false;

    // 해당 아이템 타입에 따라 추가
    if (NewItem.Data.EItemType == EItemType::Consumable)
    {
        // 중첩 가능한 아이템인 경우
        if (NewItem.Data.bIsStackable)
        {
            for (FInvItem& InvItem : Inventory_C)
            {
                // 아이디가 일치하는 경우
                if (InvItem.Data.ItemID == NewItem.Data.ItemID)
                {
                    // 이미 가득 차있으면 건너뛰기
                    if (InvItem.Data.MaxStackCount == InvItem.Quantity) continue;

                    // 아이템 남은 공간 계산 (남은 공간 : 최대 - 현재)
                    int32 TempNum = InvItem.Data.MaxStackCount - InvItem.Quantity;
                    // 남은 공간보다 새로 들어온 아이템의 수가 적은 경우
                    if (TempNum >= NewItem.Quantity)
                    {
                        // 그대로 추가하고 반환
                        InvItem.Quantity += NewItem.Quantity;
                        OnChangeInventory.Broadcast();
                        return true;
                    }
                    // 남은 공간보다 새로 들어온 아이템의 수가 많은 경우
                    else
                    {
                        // 인벤토리 아이템을 최대 치로 설정
                        InvItem.Quantity = InvItem.Data.MaxStackCount;
                        // 남은 수량 업데이트
                        NewItem.Quantity -= TempNum;
                    }
                }
            }
        }

        // 빈 공간을 찾아서 남은 데이터 삽입
        for (int32 i = 0; i < Inventory_C.Num(); i++)
        {
            // 해당 인덱스가 비어있는(초기화) 경우
            if (Inventory_C[i].bIsEmpty)
            {
                Inventory_C[i] = NewItem;
                OnChangeInventory.Broadcast();
                return true;
            }
        }		
    }
    else if (NewItem.Data.EItemType == EItemType::Miscellaneous)
    {
        // 중첩 가능한 아이템인 경우
        if (NewItem.Data.bIsStackable)
        {
            for (FInvItem& InvItem : Inventory_M)
            {
                // 아이디가 일치하는 경우
                if (InvItem.Data.ItemID == NewItem.Data.ItemID)
                {
                    // 이미 가득 차있으면 건너뛰기
                    if (InvItem.Data.MaxStackCount == InvItem.Quantity) continue;

                    // 아이템 남은 공간 계산 (남은 공간 : 최대 - 현재)
                    int32 TempNum = InvItem.Data.MaxStackCount - InvItem.Quantity;
                    // 남은 공간보다 새로 들어온 아이템의 수가 적은 경우
                    if (TempNum >= NewItem.Quantity)
                    {
                        // 그대로 추가하고 반환
                        InvItem.Quantity += NewItem.Quantity;
                        OnChangeInventory.Broadcast();
                        return true;
                    }
                    // 남은 공간보다 새로 들어온 아이템의 수가 많은 경우
                    else
                    {
                        // 인벤토리 아이템을 최대 치로 설정
                        InvItem.Quantity = InvItem.Data.MaxStackCount;
                        // 남은 수량 업데이트
                        NewItem.Quantity -= TempNum;
                    }
                }
            }
        }

        // 빈 공간을 찾아서 남은 데이터 삽입
        for (int32 i = 0; i < Inventory_M.Num(); i++)
        {
            // 해당 인덱스가 비어있는(초기화) 경우
            if (Inventory_M[i].bIsEmpty)
            {
                Inventory_M[i] = NewItem;
                OnChangeInventory.Broadcast();
                return true;
            }
        }
    }

    // 남은 수량의 아이템 반환
    Quantity = NewItem.Quantity;

    // 여기까지 온 경우 인벤토리가 가득 차서 실패한 경우이므로 false 리턴
    OnChangeInventory.Broadcast();
    return false;
}

아이템 사용/제거 기능

  • 퀵슬롯에 등록된 아이템은 사용이 가능해야 합니다.
  • 소지한 아이템의 수량이 0이되면 인벤토리에서 해당 정보가 제거되어야 합니다.
더보기
void UTA_InventoryComponent::UseItem(ESlotType Type, int32 Index)
{
    switch (Type)
    {
    case ESlotType::ST_Inventory_C:
        // 해당 아이템이 유효한지 체크
        if (Inventory_C.IsValidIndex(Index) && !Inventory_C[Index].bIsEmpty)
        {
            // 아이템 사용
            ICombatComponentInterface* CombatInterface = Cast<ICombatComponentInterface>(OwnerPlayer);
            if (CombatInterface)
            {
                CombatInterface->GetCombatComponent()->HealStat(Inventory_C[Index].Data.AddHPAmount, Inventory_C[Index].Data.AddStaminaAmount);
            }

            // 아이템 수량 감소
            Inventory_C[Index].Quantity--;
        
            OnChangeInventory.Broadcast();

            // 수량이 0인 경우
            if (Inventory_C[Index].Quantity <= 0)
            {
                // 아이템 제거
                RemoveItem(Type, Index);
            }
        }
        break;
    case ESlotType::ST_Inventory_M:
        // 해당 아이템이 유효한지 체크
        if (Inventory_M.IsValidIndex(Index) && !Inventory_M[Index].bIsEmpty)
        {
            // 아이템 수량 감소
            Inventory_M[Index].Quantity--;

            OnChangeInventory.Broadcast();

            // 수량이 0인 경우
            if (Inventory_M[Index].Quantity <= 0)
            {
                // 아이템 제거
                RemoveItem(Type, Index);
            }
        }
        break;
    }
}

void UTA_InventoryComponent::RemoveItem(ESlotType Type, int32 Index)
{
    switch (Type)
    {
    case ESlotType::ST_Inventory_C:
        if (Inventory_C.IsValidIndex(Index))
        {
            // 아이템 초기화
            Inventory_C[Index] = FInvItem();

            // 퀵 슬롯 확인 후 초기화
            for (int32 i = 0; i < QuickSlot.Num(); i++)
            {
                // 해당 인덱스를 가진 경우
                if (QuickSlot[i] == Index)
                {
                    // 초기화
                    QuickSlot[i] = -1;
                }
            }
        }
        break;
    case ESlotType::ST_Inventory_M:
        if (Inventory_M.IsValidIndex(Index))
        {
            // 아이템 초기화
            Inventory_M[Index] = FInvItem();
        }
        break;
    }

    OnChangeInventory.Broadcast();
}

아이템 교체 기능

  • 인벤토리 내부의 아이템은 드래그 & 드랍으로 Swap이 가능해야 합니다.
  • 이전 인덱스, 현재 인덱스, 슬롯의 타입을 전달받아 교환하는 함수를 작성하였습니다.
  • 해당 아이템이 퀵슬롯에 들어있는 경우 퀵슬롯에 저장하는 인덱스 정보 또한 수정해주도록 하였습니다.
더보기
void UTA_InventoryComponent::SwapItem(ESlotType Type1, int32 Index1, ESlotType Type2, int32 Index2)
{
    if (Type1 == Type2)
    {
        FInvItem Temp;

        switch (Type1)
        {
        case ESlotType::ST_Inventory_C:
            if (QuickSlot[0] == Index1 && QuickSlot[1] == Index2 || QuickSlot[0] == Index2 && QuickSlot[1] == Index1)
            {
            	QuickSlot.Swap(0, 1);
            }
            else
            {
                for (int32 i = 0; i < QuickSlot.Num(); i++)
                {
                    if (QuickSlot[i] == Index1)
                    {
                        QuickSlot[i] = Index2;
                    }
                }
            }

            Temp = Inventory_C[Index1];
            Inventory_C[Index1] = Inventory_C[Index2];
            Inventory_C[Index2] = Temp;
            break;
        case ESlotType::ST_Inventory_M:
            Temp = Inventory_M[Index1];
            Inventory_M[Index1] = Inventory_M[Index2];
            Inventory_M[Index2] = Temp;
            break;
        case ESlotType::ST_QuickSlot:
            break;
        }
    }

    OnChangeInventory.Broadcast();
    for (FInvItem Item : Inventory_C)
    {
        UE_LOG(LogTemp, Warning, TEXT("%s, %d"), *Item.Data.ItemName.ToString(), Item.Quantity);
    }
}

아이템 구매 기능

  • 상점을 통해 아이템을 구매 가능하도록 구현할 예정입니다.
  • 아이템의 이름과 가격을 전달받아 조건에 맞으면 골드를 소모하고 아이템을 추가하는 함수를 작성하였습니다.
더보기
bool UTA_InventoryComponent::PurchaseItem(FName ItemName, int32 Price)
{
    if (Gold >= Price)
    {
        int32 TempNum = 1;
        // 아이템 추가에 성공했다면?
        if (AddItem(ItemName, TempNum))
        {
            // 소지 골드 수정
            Gold -= Price;

            if (PurchaseSound)
            {
                UGameplayStatics::PlaySound2D(GetWorld(), PurchaseSound);
            }

            OnChangeGold.Broadcast();
            return true;
        }
    }

    return false;
}

퀵슬롯 등록 기능

  • 퀵슬롯은 실제 인벤토리의 인덱스만을 저장하도록 만들었습니다.
  • 2개의 슬롯이 준비되어 있으며, 전달받은 인벤토리의 타입이 소비 타입인 경우 인덱스를 저장합니다.
더보기
void UTA_InventoryComponent::AddQuickSlot(ESlotType Type, int32 Index1, int32 Index2)
{
    // 슬롯 타입이 소비 슬롯인 경우
    if (Type == ESlotType::ST_Inventory_C)
    {
        // 소비 슬롯의 해당 칸의 인덱스를 복제합니다.
        QuickSlot[Index2] = Index1;
    }

    OnChangeInventory.Broadcast();
}
728x90
반응형