과제 목표
- 가방은 TArray로 만든다.
- TMap은 가방에 담긴 아이템을 확인하면 그 아이템 정보가 Key 값으로 조회되도록 만든다.
- TSet은 칭호 획득에 사용한다.
- 해당 칭호가 있어야 아이템을 사용할 수 있도록 만든다.
각자 가장 잘하는 일을 맡겨 하나의 시스템으로 엮기
| 가방 | 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 (칭호)
'학습 > Unreal' 카테고리의 다른 글
| NetMode, NetConnection, NetDriver, Ownership (0) | 2026.06.18 |
|---|---|
| Dedicated Server, 서버-클라이언트 구조의 핵심 특징 (0) | 2026.06.17 |
| TObjectPtr / TSubclassOf / TArray / TSet / TMap - 언리얼 주요 타입과 컨테이너 정리 (0) | 2026.06.04 |
| 언리얼 접두사 규칙과 모듈 구조 (1) | 2026.06.02 |
| 리플렉션, GC, CDO, 메모리 (0) | 2026.05.28 |
