1. 오늘의 개발 개요
Part 3에서 아이템 클래스와 오버랩 감지를 완성한 데 이어, 이번 파트에서는 인벤토리 시스템의 핵심 백엔드 로직을 구현했습니다. 아이템 크기(FIntPoint Dimensions) 정의부터, 1차원 배열을 2차원 그리드로 매핑하는 좌표 변환, 공간 가용성 체크, 실제 배열 등록까지 완성했습니다.

2. 아이템 크기 정의 — FIntPoint Dimensions
각 아이템이 인벤토리 그리드에서 차지하는 칸 수를 FIntPoint로 선언했습니다. FVector2D와 유사하지만 정수형(X, Y)이라 타일 인덱스 계산에 적합합니다. EditAnywhere로 노출해 블루프린트마다 개별 설정이 가능합니다.
// ItemBase.h
protected:
UPROPERTY(EditAnywhere, Category = "Item Info | Dimensions")
FIntPoint Dimensions; // X = 가로 칸 수, Y = 세로 칸 수
public:
FIntPoint GetDimensions() const;
// ItemBase.cpp
FIntPoint AItemBase::GetDimensions() const
{
return Dimensions;
}
블루프린트별 아이템 크기 설정 예시
| 아이템 | Dimensions.X (가로) | Dimensions.Y (세로) |
| AK-47 | 4 | 2 |
| 수류탄 | 1 | 1 |
| 단검 | 1 | 2 |
3. Items 배열 선언 & 초기화
인벤토리의 각 타일과 1:1로 대응하는 TArray<AItemBase*> Items를 InventoryComponent에 선언합니다. 배열 크기는 Columns × Rows이며, BeginPlay 시점에 모두 nullptr로 초기화합니다. 초기화는 InventoryComponent의 BeginPlay가 아닌 캐릭터의 BeginPlay에서 처리한 점이 핵심입니다.
// InventoryComponent.h
TArray<AItemBase*> Items; // 타일 수만큼 크기, 빈 타일 = nullptr
bool TryAddItem(AItemBase* ItemToAdd);
bool IsRoomAvailable(AItemBase* ItemToAdd, int32 Index);
FIntPoint IndexToTile(int32 TopLeftIndex);
bool IsTileValid(FIntPoint Tile);
int32 TileToIndex(FIntPoint Tile);
bool GetResultAtIndex(int32 Index);
AItemBase* GetItemAtIndex(int32 Index);
void AddItemAt(AItemBase* ItemToAdd, int32 TopLeftIndex);
// IntentorySystemCppCharacter.cpp — BeginPlay에서 배열 크기 확정
void AIntentorySystemCppCharacter::BeginPlay()
{
Super::BeginPlay();
// ... 위젯 생성 코드 ...
InventoryComponent->Items.Init(nullptr,
InventoryComponent->Columns * InventoryComponent->Rows);
// Columns=10, Rows=6이면 Items 크기 = 60, 전부 nullptr로 초기화
}
※ Init() vs SetNum()
- Items.Init(nullptr, N) — 크기를 N으로 설정하고 모든 요소를 nullptr로 채움. 빈 인벤토리 초기화에 적합.
- Items.SetNum(N) — 크기만 N으로 맞추고 기존 값 유지. 포인터 배열의 경우 초기값이 보장되지 않아 Init이 더 안전.
4. 1D ↔ 2D 좌표 변환 — IndexToTile / TileToIndex
// InventoryComponent.cpp
FIntPoint UInventoryComponent::IndexToTile(int32 Index)
{
// X = 열(Column), Y = 행(Row)
return FIntPoint(Index % Columns, Index / Columns);
}
int32 UInventoryComponent::TileToIndex(FIntPoint Tile)
{
return Tile.X + Tile.Y * Columns;
}

▲
진하게 표시된 칸 = 2×2 아이템이 점유한 타일 (idx 6,7,10,11)
※ 변환 공식 직관적으로 이해하기
- Index → Tile: X = Index % Columns (열 위치), Y = Index / Columns (행 위치 — 정수 나눗셈)
- Tile → Index: X + Y * Columns — Y행에서 X번째 칸의 절대 위치
- 이 두 함수가 없으면 이후 모든 공간 계산 로직이 작동 불가 — 인벤토리 시스템의 수학적 기반
5. 공간 확인 로직 — IsRoomAvailable
특정 인덱스를 좌상단(TopLeft)으로 삼아 아이템 크기만큼의 공간이 비어 있는지 확인합니다. 이중 for문으로 아이템이 점유할 모든 타일을 순회하며 두 가지 조건을 체크합니다.
// InventoryComponent.cpp
bool UInventoryComponent::IsRoomAvailable(AItemBase* ItemToAdd, int32 TopLeftIndex)
{
FIntPoint Dimensions = ItemToAdd->GetDimensions();
FIntPoint Tile = IndexToTile(TopLeftIndex); // 1D → 2D 변환
// 아이템이 점유할 모든 타일 순회
for (int32 i = Tile.X; i <= Tile.X + Dimensions.X - 1; i++) {
for (int32 j = Tile.Y; j <= Tile.Y + Dimensions.Y - 1; j++) {
if (IsTileValid(FIntPoint(i, j))) { // ① 경계 안인지
int32 Index = TileToIndex(FIntPoint(i, j));
if (GetResultAtIndex(Index)) { // ② 유효한 인덱스인지
if (GetItemAtIndex(Index)) { // ③ 이미 아이템이 있는지
return false; // 점유됨 → 배치 불가
}
}
else { return false; }
}
else { return false; } // 경계 밖 → 배치 불가
}
}
return true; // 모든 타일 통과 → 배치 가능
}
// 보조 함수들
// 타일이 인벤토리 경계 내에 있는지 확인
bool UInventoryComponent::IsTileValid(FIntPoint Tile)
{
return (Tile.X >= 0 && Tile.Y >= 0 && Tile.X < Columns && Tile.Y < Rows);
}
// 1D 인덱스가 배열 범위 내인지 확인
bool UInventoryComponent::GetResultAtIndex(int32 Index)
{
return Items.IsValidIndex(Index);
}
// 해당 인덱스의 아이템 포인터 반환 (없으면 nullptr)
AItemBase* UInventoryComponent::GetItemAtIndex(int32 Index)
{
if (Items.IsValidIndex(Index)) { return Items[Index]; }
else { return nullptr; }
}
※ 3단계 검증 구조
- ① IsTileValid — 인벤토리 경계 밖으로 아이템이 삐져나가는지 체크 (X, Y 모두 0 이상 & Columns/Rows 미만)
- ② GetResultAtIndex — IsValidIndex()로 배열 접근 전 안전성 보장 (out-of-bounds 방지)
- ③ GetItemAtIndex — 해당 타일이 nullptr이면 비어 있음, 포인터가 있으면 점유 중
6. 아이템 추가 — TryAddItem & AddItemAt
TryAddItem이 배열 전체를 순회하며 첫 번째로 가용한 공간을 찾고, AddItemAt이 해당 위치에 아이템 포인터를 실제로 등록합니다.
// InventoryComponent.cpp — TryAddItem
bool UInventoryComponent::TryAddItem(AItemBase* ItemToAdd)
{
if (ItemToAdd)
{
for (int32 i = 0; i < Items.Num(); i++) {
if (IsRoomAvailable(ItemToAdd, i)) { // 공간 확인
AddItemAt(ItemToAdd, i); // 배열에 등록
return true;
}
}
return false; // 공간 없음
}
return false;
}
// InventoryComponent.cpp — AddItemAt
void UInventoryComponent::AddItemAt(AItemBase* ItemToAdd, int32 TopLeftIndex)
{
FIntPoint Dimensions = ItemToAdd->GetDimensions();
FIntPoint Tile = IndexToTile(TopLeftIndex);
// 아이템 크기만큼 모든 점유 타일에 포인터 등록
for (int32 i = Tile.X; i <= Tile.X + Dimensions.X - 1; i++) {
for (int32 j = Tile.Y; j <= Tile.Y + Dimensions.Y - 1; j++) {
Items[TileToIndex(FIntPoint(i, j))] = ItemToAdd;
}
}
}
※ 같은 포인터를 여러 타일에 등록하는 이유
- AK-47(4×2)를 배치하면 점유하는 8칸 모두에 같은 포인터를 저장
- 이후 어떤 타일을 클릭해도 동일한 아이템 참조를 얻을 수 있어 아이템 이동/제거 시 편리
- 아이템 제거 시에는 역으로 모든 관련 타일을 nullptr로 초기화하면 됨
7. OnBeginOverlap에 백엔드 연결 & Destroy()
Part 3에서 디버그 메시지만 출력하던 OnBeginOverlap에 실제 인벤토리 추가 로직을 연결하고, 성공 시 월드의 아이템 액터를 Destroy()해 시각적으로 획득을 표현합니다.
// IntentorySystemCppCharacter.cpp — Part 3 → Part 4 변경점
void AIntentorySystemCppCharacter::OnBeginOverlap( ... )
{
AItemBase* Item = Cast<AItemBase>(OtherActor);
if (Item)
{
// Part 3: GEngine->AddOnScreenDebugMessage(...)
// Part 4: TryAddItem으로 교체 + 성공 시 Destroy
if (InventoryComponent->TryAddItem(Item))
{
OtherActor->Destroy(); // 인벤토리에 추가됐으면 월드에서 제거
}
// false 반환 시 = 공간 부족 → 아이템 그대로 유지
}
}
※ Destroy() 타이밍
- TryAddItem이 true를 반환한 경우에만 Destroy() 호출 → 인벤토리가 꽉 찼을 때 아이템이 바닥에 그대로 남음
- 아직 AddItemAt에서 Destroy()를 직접 호출하지 않고, 호출부(OnBeginOverlap)에서 처리 → 책임 분리 유지
8. 개발 중 직면한 문제 & 해결
① Items 배열 크기가 0인 상태에서 접근 → 크래시
🔴 문제
InventoryComponent::BeginPlay에서 배열을 초기화하려 했으나, Columns와 Rows 값이 에디터에서 설정되기 전에 BeginPlay가 호출되어 크기가 0으로 초기화되는 문제 발생.
✅ 해결
배열 초기화를 InventoryComponent::BeginPlay가 아닌 캐릭터의 BeginPlay에서 처리. InventoryComponent->Items.Init(nullptr, InventoryComponent->Columns * InventoryComponent->Rows) 로 에디터 설정값이 확정된 시점에 배열 크기를 할당.
9. 오늘 배운 점 & 회고
- 1D 배열로 2D 그리드 표현 — IndexToTile / TileToIndex 두 함수가 전체 공간 로직의 수학적 기반
- FIntPoint는 정수형 2D 좌표가 필요할 때 FVector2D보다 타일 인덱스 계산에 적합
- 아이템 크기만큼 같은 포인터를 여러 타일에 등록하면 어느 타일을 참조해도 동일한 아이템에 접근 가능
- 배열 초기화 타이밍은 값이 확정된 시점(캐릭터 BeginPlay)을 잘 따져야 함
※ 핵심 교훈
"공간 인벤토리의 핵심은 1D 배열을 2D처럼 다루는 좌표 변환이다. Index % Columns와 Index / Columns 공식만 잘 이해하면 경계 체크, 공간 확인, 아이템 등록 모두 자연스럽게 따라온다. 그리고 배열 접근 전에는 항상 IsValidIndex로 안전을 확보하자."
'Unreal' 카테고리의 다른 글
| C++을 사용한 언리얼엔진 인벤토리 시스템 Part.6 (0) | 2026.03.20 |
|---|---|
| C++을 사용한 언리얼엔진 인벤토리 시스템 Part.5 (0) | 2026.03.18 |
| C++을 사용한 언리얼엔진 인벤토리 시스템 Part.3 (0) | 2026.03.12 |
| C++을 사용한 언리얼엔진 인벤토리 시스템 Part.2 (0) | 2026.03.10 |
| C++을 사용한 언리얼엔진 인벤토리 시스템 Part.1 (0) | 2026.03.09 |