인벤토리 컴포넌트
슬롯 기반 인벤토리 시스템 구현
인벤토리 시스템 개요
액터 컴포넌트 기반 인벤토리 설계
인벤토리 컴포넌트는 플레이어 또는 NPC가 아이템을 소지하고 관리하는 기능을 담당합니다. ActorComponent로 구현하여 어떤 액터에든 부착할 수 있습니다.
인벤토리 슬롯 구조
각 슬롯은 UItemInstance를 참조합니다
주요 델리게이트
OnInventoryChanged- 인벤토리 내용 변경 시OnItemEquipped- 아이템 장착 시
컴포넌트 클래스 정의
인벤토리 컴포넌트 헤더 파일
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ItemDefinition.h"
#include "InventoryComponent.generated.h"
class UItemInstance;
class UItemDefinition;
// 델리게이트 선언
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FOnInventoryChanged, UItemInstance*, Item, int32, SlotIndex);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FOnItemEquipped, UItemInstance*, Item, EEquipmentSlot, Slot);
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYGAME_API UInventoryComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInventoryComponent();
// 인벤토리 크기
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inventory")
int32 InventorySize = 30;
// 델리게이트
UPROPERTY(BlueprintAssignable, Category = "Inventory")
FOnInventoryChanged OnInventoryChanged;
UPROPERTY(BlueprintAssignable, Category = "Inventory")
FOnItemEquipped OnItemEquipped;
// ===== 아이템 추가 =====
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool AddItem(const UItemDefinition* ItemDef, int32 Count = 1);
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool AddItemInstance(UItemInstance* Item);
// ===== 아이템 제거 =====
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool RemoveItem(int32 SlotIndex, int32 Count = 1);
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool RemoveItemByDefinition(const UItemDefinition* ItemDef, int32 Count = 1);
// ===== 아이템 사용 =====
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool UseItem(int32 SlotIndex);
// ===== 장비 관련 =====
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool EquipItem(int32 InventorySlotIndex);
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool UnequipItem(EEquipmentSlot Slot);
UFUNCTION(BlueprintPure, Category = "Inventory")
UItemInstance* GetEquippedItem(EEquipmentSlot Slot) const;
// ===== 조회 =====
UFUNCTION(BlueprintPure, Category = "Inventory")
UItemInstance* GetItemAt(int32 SlotIndex) const;
UFUNCTION(BlueprintPure, Category = "Inventory")
int32 GetItemCount(const UItemDefinition* ItemDef) const;
UFUNCTION(BlueprintPure, Category = "Inventory")
bool HasItem(const UItemDefinition* ItemDef, int32 Count = 1) const;
UFUNCTION(BlueprintPure, Category = "Inventory")
TArray<UItemInstance*> GetAllItems() const;
// ===== 정렬 =====
UFUNCTION(BlueprintCallable, Category = "Inventory")
void SortInventory();
protected:
virtual void BeginPlay() override;
UPROPERTY()
TArray<TObjectPtr<UItemInstance>> InventorySlots;
UPROPERTY()
TMap<EEquipmentSlot, TObjectPtr<UItemInstance>> EquippedItems;
private:
int32 FindEmptySlot() const;
int32 FindStackableSlot(const UItemDefinition* ItemDef) const;
void ApplyEquipmentEffects(UItemInstance* Item, bool bApply);
};
아이템 추가 구현
스택 처리를 포함한 아이템 추가 로직
#include "InventoryComponent.h"
#include "ItemDefinition.h"
#include "ItemInstance.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystemInterface.h"
UInventoryComponent::UInventoryComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UInventoryComponent::BeginPlay()
{
Super::BeginPlay();
// 인벤토리 슬롯 초기화
InventorySlots.SetNum(InventorySize);
}
bool UInventoryComponent::AddItem(const UItemDefinition* ItemDef, int32 Count)
{
if (!ItemDef || Count <= 0) return false;
// 스택 가능 여부 확인
const UItemFragment_Stackable* StackFragment =
ItemDef->FindFragment<UItemFragment_Stackable>();
const int32 MaxStack = StackFragment ? StackFragment->MaxStackSize : 1;
int32 RemainingCount = Count;
// 기존 스택에 추가 시도
while (RemainingCount > 0 && MaxStack > 1)
{
int32 StackableSlot = FindStackableSlot(ItemDef);
if (StackableSlot == INDEX_NONE) break;
UItemInstance* ExistingItem = InventorySlots[StackableSlot];
int32 SpaceInStack = MaxStack - ExistingItem->StackCount;
int32 ToAdd = FMath::Min(SpaceInStack, RemainingCount);
ExistingItem->StackCount += ToAdd;
RemainingCount -= ToAdd;
OnInventoryChanged.Broadcast(ExistingItem, StackableSlot);
}
// 새 슬롯에 추가
while (RemainingCount > 0)
{
int32 EmptySlot = FindEmptySlot();
if (EmptySlot == INDEX_NONE)
{
// 인벤토리 가득 참
return false;
}
UItemInstance* NewItem = NewObject<UItemInstance>(this);
int32 ToAdd = FMath::Min(MaxStack, RemainingCount);
NewItem->Initialize(ItemDef, ToAdd);
InventorySlots[EmptySlot] = NewItem;
RemainingCount -= ToAdd;
OnInventoryChanged.Broadcast(NewItem, EmptySlot);
}
return true;
}
FindEmptySlot()
비어있는 첫 번째 슬롯의 인덱스를 반환합니다. 모두 차있으면 INDEX_NONE을 반환합니다.
FindStackableSlot()
같은 아이템 정의를 가지고 있으면서 스택 여유가 있는 슬롯을 찾습니다.
int32 UInventoryComponent::FindEmptySlot() const
{
for (int32 i = 0; i < InventorySlots.Num(); ++i)
{
if (!InventorySlots[i])
{
return i;
}
}
return INDEX_NONE;
}
int32 UInventoryComponent::FindStackableSlot(const UItemDefinition* ItemDef) const
{
const UItemFragment_Stackable* StackFragment =
ItemDef->FindFragment<UItemFragment_Stackable>();
if (!StackFragment) return INDEX_NONE;
for (int32 i = 0; i < InventorySlots.Num(); ++i)
{
UItemInstance* Item = InventorySlots[i];
if (Item && Item->ItemDefinition == ItemDef)
{
if (Item->StackCount < StackFragment->MaxStackSize)
{
return i;
}
}
}
return INDEX_NONE;
}
아이템 사용
소비 아이템과 GAS 연동
bool UInventoryComponent::UseItem(int32 SlotIndex)
{
if (!InventorySlots.IsValidIndex(SlotIndex)) return false;
UItemInstance* Item = InventorySlots[SlotIndex];
if (!Item || !Item->ItemDefinition) return false;
// 소비 아이템 프래그먼트 확인
const UItemFragment_Consumable* ConsumableFragment =
Item->ItemDefinition->FindFragment<UItemFragment_Consumable>();
if (!ConsumableFragment) return false;
// GameplayEffect 적용
if (ConsumableFragment->UseEffect)
{
AActor* Owner = GetOwner();
if (IAbilitySystemInterface* ASI = Cast<IAbilitySystemInterface>(Owner))
{
if (UAbilitySystemComponent* ASC = ASI->GetAbilitySystemComponent())
{
FGameplayEffectContextHandle Context = ASC->MakeEffectContext();
Context.AddSourceObject(Item);
FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(
ConsumableFragment->UseEffect, 1, Context);
if (Spec.IsValid())
{
ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get());
}
}
}
}
// 소모
if (ConsumableFragment->bConsumeOnUse)
{
RemoveItem(SlotIndex, 1);
}
return true;
}
bool UInventoryComponent::RemoveItem(int32 SlotIndex, int32 Count)
{
if (!InventorySlots.IsValidIndex(SlotIndex)) return false;
UItemInstance* Item = InventorySlots[SlotIndex];
if (!Item) return false;
Item->StackCount -= Count;
if (Item->StackCount <= 0)
{
InventorySlots[SlotIndex] = nullptr;
}
OnInventoryChanged.Broadcast(InventorySlots[SlotIndex], SlotIndex);
return true;
}
포션의 효과는 GameplayEffect로 정의하여 힐량, 버프 지속시간 등을 데이터로 관리할 수 있습니다. 블루프린트에서 새 포션 종류를 만들 때 C++ 코드 수정 없이 효과만 정의하면 됩니다.
조회 함수
인벤토리 데이터 접근 함수
UItemInstance* UInventoryComponent::GetItemAt(int32 SlotIndex) const
{
if (InventorySlots.IsValidIndex(SlotIndex))
{
return InventorySlots[SlotIndex];
}
return nullptr;
}
int32 UInventoryComponent::GetItemCount(const UItemDefinition* ItemDef) const
{
int32 TotalCount = 0;
for (UItemInstance* Item : InventorySlots)
{
if (Item && Item->ItemDefinition == ItemDef)
{
TotalCount += Item->StackCount;
}
}
return TotalCount;
}
bool UInventoryComponent::HasItem(const UItemDefinition* ItemDef, int32 Count) const
{
return GetItemCount(ItemDef) >= Count;
}
TArray<UItemInstance*> UInventoryComponent::GetAllItems() const
{
TArray<UItemInstance*> Result;
for (UItemInstance* Item : InventorySlots)
{
if (Item)
{
Result.Add(Item);
}
}
return Result;
}
UItemInstance* UInventoryComponent::GetEquippedItem(EEquipmentSlot Slot) const
{
if (const TObjectPtr<UItemInstance>* Found = EquippedItems.Find(Slot))
{
return *Found;
}
return nullptr;
}
핵심 요약
- ActorComponent 기반 — 인벤토리를 컴포넌트로 구현하여 플레이어, NPC, 컨테이너 등에 재사용 가능합니다.
- 델리게이트 활용 — OnInventoryChanged, OnItemEquipped 델리게이트로 UI와 느슨하게 결합합니다.
- 스택 처리 — Stackable Fragment를 가진 아이템은 MaxStackSize까지 같은 슬롯에 누적됩니다.
- GAS 연동 — 소비 아이템 사용 시 GameplayEffect를 자동 적용합니다.
- TObjectPtr 사용 — 가비지 컬렉션을 지원하는 스마트 포인터로 아이템을 관리합니다.
도전 과제
배운 내용을 직접 실습해보세요
UActorComponent를 상속한 URPGInventoryComponent를 구현하세요. TArray로 아이템 슬롯을 관리하고, AddItem(), RemoveItem(), GetItemAtSlot() 함수를 BlueprintCallable로 노출하세요.
MaxSlots 제한, 아이템 스택(같은 아이템 합치기), 슬롯 스왑 기능을 구현하세요. OnInventoryChanged 델리게이트를 BlueprintAssignable로 선언하여 UI 업데이트를 연동하세요.
인벤토리 데이터를 FFastArraySerializer(FRPGInventoryList)로 구현하여 효율적인 네트워크 복제를 달성하세요. 아이템 추가/제거 시 변경된 슬롯만 복제하는 최적화를 구현하세요.