과제 목표

  1. 가방은 TArray로 만든다.
  2. TMap은 가방에 담긴 아이템을 확인하면 그 아이템 정보가 Key 값으로 조회되도록 만든다.
  3. TSet은 칭호 획득에 사용한다.
  4. 해당 칭호가 있어야 아이템을 사용할 수 있도록 만든다.

각자 가장 잘하는 일을 맡겨 하나의 시스템으로 엮기

가방 TArray<int32>
아이템 DB TMap<int32, FInventoryItemInfo>
칭호 TSet<FName>
사용 조건 칭호 검사 로직

 

중요한 설계 결정 : 가방에 아이템 정보를 통째로 넣지 않고 ID만 넣는다

만약 가방 슬롯마다 "아이언 소드, 가격 150, 무게 3.5, 설명..."을 전부 저장한다면 같은 아이템을 10개 가질 때 같은 정보가 10번 중복 저장된다.

메모리 낭비일 뿐 아니라 아이템 가격을 한 번 수정하려면 10군데를 다 고쳐야 한다.

그래서 가방은 가벼운 ID만 들고, 무거운 실제 정보는 DB 한 곳에만 둔다.

필요할 때 ID로 조회한다. 

칭호는 왜 TSet인가

등급은 "있다 / 없다"만 중요하지 중복이 의미 없음.

게다가 등급 확인을 매 아이템 사용 시점마다 빠르게(O(1)) 확인해야 함

중복 방지 + 빠른 조회 → TSet


데이터 구조 설계

아이템 정보 구조체

TMap의 Value로 들어갈 아이템 한 개의 정보를 묶은 구조체

USTRUCT(BlueprintType)
struct FInventoryItemInfo
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FString ItemName;        // 아이템 이름

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 Price = 0;         // 가격

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName RequiredTitle;     // 이 아이템을 쓰려면 필요한 칭호
};

 

 

FName을 쓴 이유: 칭호 이름은 "Warrior", "Mage"처럼 정해진 식별자. FName은 내부적으로 숫자로 변환해 비교가 O(1)이라 이런 태그/식별자 용도에 적합하다. FString은 글자 단위 비교라 느리다.

 

세 컨테이너 선언

TArray<int32>                    Bag;             // 1. 가방: 아이템 ID 목록
TMap<int32, FInventoryItemInfo>  ItemDatabase;    // 2. DB: ID → 아이템 정보
TSet<FName>                      AcquiredTitles;  // 3. 획득한 칭호

이 세 변수는 int32 / FName / 일반 구조체를 담으므로 GC 추적 대상이 아니다. 따라서 UPROPERTY를 붙이지 않아도 된다. (UObject나 액터를 담는다면 그때는 붙여야 한다.)

 


구현

전체를 Actor 클래스(AInventoryManager)에 담았다. (학습용이니까)

아이템 DB 채우기

void AInventoryManager::BeginPlay()
{
    Super::BeginPlay();
    InitItemDatabase();
}

void AInventoryManager::InitItemDatabase()
{
    // Key = 아이템 ID, Value = 아이템 정보
    // RequiredTitle이 NAME_None이면 칭호 없이도 사용 가능
    ItemDatabase.Add(1001, FInventoryItemInfo{ TEXT("Wooden Sword"),  50,  NAME_None });
    ItemDatabase.Add(1002, FInventoryItemInfo{ TEXT("Iron Sword"),    150, FName("Warrior") });
    ItemDatabase.Add(1003, FInventoryItemInfo{ TEXT("Magic Staff"),   300, FName("Mage") });
    ItemDatabase.Add(1004, FInventoryItemInfo{ TEXT("Health Potion"), 30,  NAME_None });
}

 

Add(Key, Value)로 등록한다.

FInventoryItemInfo{ ... }는 구조체를 즉석에서 만드는 표현으로, 중괄호 안 값이 멤버 선언 순서대로 들어간다.

 

가방에 담기 + 가방 확인(DB 조회)

void AInventoryManager::AddItemToBag(int32 ItemID)
{
    // DB에 존재하는 아이템만 가방에 넣는다 (없는 ID를 넣으면 조회가 안 되므로)
    if (ItemDatabase.Contains(ItemID))
    {
        Bag.Add(ItemID);  // 가방엔 ID만! 정보는 DB에 이미 있다
    }
}

void AInventoryManager::PrintBag()
{
    // 가방(TArray)을 순회하며, 각 ID로 DB(TMap)를 조회한다
    for (int32 ItemID : Bag)
    {
        FInventoryItemInfo* Info = ItemDatabase.Find(ItemID);  // Value 포인터 반환
        if (Info)  // 찾았으면 (nullptr 아니면)
        {
            UE_LOG(LogTemp, Log, TEXT("[%d] %s / 가격 %d / 필요칭호 %s"),
                ItemID, *Info->ItemName, Info->Price, *Info->RequiredTitle.ToString());
        }
    }
}

 

가방엔 ID만 들어 있고, 출력할 때 DB에서 이름·가격·칭호를 꺼내온다.

  • ItemDatabase.Find(ItemID)는 Value의 포인터를 반환하고, 없으면 nullptr이다. 그래서 다음 줄에서 if (Info)로 확인한다.
    만약 ItemDatabase[ItemID](대괄호)를 썼다면 없는 Key일 때 크래시가 난다. 
  • *Info->ItemName의 *는 포인터 역참조가 아니라, FString을 출력 가능한 문자열(TCHAR)로 바꾸는 기호다.
    언리얼에서 문자열을 %s로 출력할 때 붙는 관용구

칭호 획득 + 사용 조건

void AInventoryManager::AcquireTitle(FName Title)
{
    AcquiredTitles.Add(Title);  // TSet — 같은 칭호 두 번 넣어도 자동 무시
}

void AInventoryManager::UseItem(int32 ItemID)
{
    // 1. 가방에 있는 아이템인지 확인
    if (!Bag.Contains(ItemID))
    {
        return;
    }

    // 2. DB에서 정보 조회
    FInventoryItemInfo* Info = ItemDatabase.Find(ItemID);
    if (!Info) { return; }

    // 3. 칭호 조건 검사 
    bool bNoTitleNeeded = (Info->RequiredTitle == NAME_None);
    bool bHasTitle      = AcquiredTitles.Contains(Info->RequiredTitle);

    if (bNoTitleNeeded || bHasTitle)
    {
        UE_LOG(LogTemp, Log, TEXT("%s 사용 성공!"), *Info->ItemName);
        // 실전이라면 여기서 효과 적용 + 가방에서 제거
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("%s 사용 불가 — '%s' 칭호 필요"),
            *Info->ItemName, *Info->RequiredTitle.ToString());
    }
}

 

핵심은 조건문 bNoTitleNeeded || bHasTitle 

  • 필요 칭호가 NAME_None이면 → 누구나 사용 가능 (포션, 나무 검 등)
  • 아니면 → AcquiredTitles.Contains(...)로 해당 칭호 보유 여부를 O(1)로 확인

AcquiredTitles.Contains가 O(1)인 점이 중요하다. 아이템을 쓸 때마다 칭호를 검사하므로 이 조회가 빨라야 게임이 버벅이지 않는다.

early return 패턴: "가방에 없으면 볼 것도 없이 바로 끝"이라고 위에서 먼저 걸러낸다. 아래 코드가 if 안에 깊게 중첩되지 않아 읽기 편해진다.

 


동작 확인

AddItemToBag(1001);  // Wooden Sword (칭호 불필요)
AddItemToBag(1003);  // Magic Staff (Mage 칭호 필요)

UseItem(1001);       // 성공 (칭호 불필요)
UseItem(1003);       // 실패 (아직 Mage 칭호 없음)

AcquireTitle(FName("Mage"));  // Mage 칭호 획득

UseItem(1003);       // 이제 성공

 

 


사용된 핵심 개념 정리

TMap::Add(Key, Value) Key-Value 등록. 같은 Key면 Value 덮어쓰기
TMap::Contains(Key) Key 존재 여부 확인, O(1)
TMap::Find(Key) Value의 포인터 반환, 없으면 nullptr (안전한 조회)
TArray::Add(값) 배열 맨 뒤에 추가, O(1)
TArray::Contains(값) 값 존재 여부, O(n) (선형 탐색)
TSet::Add(값) 중복 자동 무시
TSet::Contains(값) 값 존재 여부, O(1) (해시)
범위 기반 for문 for (int32 ID : Bag) — 원소를 하나씩 꺼내 순회
포인터 -> 포인터가 가리키는 객체의 멤버 접근
if (Info) 포인터 nullptr 확인 (!= nullptr 줄임)
*FString FString을 출력용 TCHAR*로 변환
FName::ToString() FName을 FString으로 변환
NAME_None 빈 FName. "값 없음"의 sentinel로 활용

컨테이너별 복잡도 비교

  TArray TSet TMap
인덱스 접근 O(1) 불가
탐색(Contains) O(n) O(1)
삽입 O(1) (맨 뒤) O(1)
순서 보장 O X
중복 허용 O X

 

이 과제에서 Bag.Contains(TArray)는 O(n)이고 AcquiredTitles.Contains(TSet)는 O(1)이다.

같은 Contains라도 자료구조에 따라 속도가 다르다

 


배운 점

세 컨테이너 중 무엇이 더 좋다는 없다

각자 구조에서 오는 성질이 다르고 문제에 맞는 걸 고르는 것이 핵심

  • 순서·인덱스가 중요하고 순회 위주 → TArray (가방)
  • Key로 정보를 즉시 조회 → TMap (아이템 DB)
  • 중복 방지 + 빠른 존재 확인 → TSet (칭호)

+ Recent posts