인벤토리 시스템 Part1 - 기본 구성과 아이템 구조
일반적인 인벤토리 시스템을 위한 아이템 액터 설계
개요
https://github.com/Reitbe/IVAN
- 정말 일반적인 인벤토리 시스템을 만들어 보았다!
- 인벤토리 시스템을 구성하는 기본요소들에 대하여 설명한다.
- 인벤토리에 저장되는 아이템이 어떤 구조로 작동하는지 설명한다.
기본 구성
간단한 구조
- 캐릭터는 장비, 스탯, 인벤토리 컴포넌트를 가지고, 관련 인터페이스들을 구현한다.
- 플레이어 컨트롤러는 HUD를, HUD는 인벤토리 위젯 구조와 퀵슬롯을 가진다. 인벤토리 위젯은 베이스-위젯-슬롯의 3단 구조이다.
- 전반적인 흐름은 위 이미지와 같다.
어떠한 기능들이 필요한가?
- 아이템
- 레벨에 놓인 아이템을 획득하여 인벤토리에 추가할 수 있다.
- 인벤토리에 있는 아이템을 레벨에 버릴 수 있다.
- 인벤토리 내부에서 아이템을 사용하거나 장착할 수 있다.
- 아이템의 세부 정보는 구조체로 저장되며, 데이터베이스에서 이를 관리한다.
- UI
- 인벤토리 위젯을 드래그하여 위치를 조정할 수 있다.
- 퀵 슬롯에 아이템을 배치하면 HUD에서 이를 확인할 수 있다.
- 아이템은 드래그 앤 드롭 혹은 더블클릭으로 사용하거나 장착할 수 있다.
- 특정 슬롯 간 스왑이 지원된다.
- 내부 구조
- 아이템의 추가 / 삭제 / 사용 / 탐색 기능을 제공한다.
- 인벤토리가 업데이트 되었음을 주변 요소들에 전파한다.
- 장비 장착에 따른 캐릭터의 스탯 및 외관을 변경한다.
아이템 구조
아이템 기본 정보
본 프로젝트에서의 아이템은 무기 / 방어구 / 소모품의 세 가지로 구분되며, 모두 IVItemBase
를 상속받는 개별 클래스이다.
IVItemBase
는 아이템들이 공통으로 가지는 기본 클래스이다. 외형을 위한 메쉬 컴포넌트와 아이템 정보 구조체를 가지며, 구조체에 정보를 채우기 위한 함수와 레벨에 스폰하기 위한 함수들을 보유한다.
또한, IIIVInteractableInterface
를 상속받기에 UIVInteractionComponent
를 상속받는 대상(플레이어)과 상호작용이 가능하다. 상호작용을 위한 범위 콜라이더와 위젯, 상호작용 및 위젯 활성화 조정 등의 함수를 가진다.
IVItemBase
를 상속받는 클래스 중 IVWeapon
은 공격을 위한 여러 멤버를 가진다. 이는 공격 시스템 포스트에서 확인할 수 있다. 방어구와 소모품 클래스는 아직 별다른 기능을 가지지 않는다.
아이템 구조체
아이템의 가장 핵심적인 정보들을 담은 구조체이다. 인벤토리 내의 각종 동작들이 해당 구조체를 중심으로 작동한다. 모든 아이템 액터는 고유의 구조체 인스턴스를 가진다.
// IVGenericStructs.h
USTRUCT(BlueprintType)
struct FItemBaseInfo
{
GENERATED_USTRUCT_BODY()
/* 아이템 ID */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FName ItemID;
/* 아이템 타입 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
EItemType ItemType;
/* 아이템 이름 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FString ItemName;
/* 아이템 설명 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FString ItemDescription;
/* 아이템을 인벤토리에 쌓아서 저장할수 있는지 여부*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
uint8 bIsStackable : 1;
/* 아이템 수량 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 ItemCount;
/* 아이템 아이콘 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
UTexture2D* ItemIcon;
/* 아이템 스태틱 메쉬 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
TObjectPtr<UStaticMesh> ItemMesh;
/* 아이템 스켈레탈 메쉬 - 장비용 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
TObjectPtr<USkeletalMesh> ItemSkeletalMesh;
/* 아이템 기본 스탯 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FBaseStat ItemStat;
/* 아이템 데미지 스탯 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FBaseDamageStat ItemDamageStat;
/* 아이템 장착 시 소켓 -> 추후 Enum 사용 고려*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FName EquipSocket;
...
}
ItemID
는 각 아이템의 정보를 식별하고 데이터베이스에서 일관되게 정보를 로드하는데 사용되며, ItemType
은 무기/방어구/소모품 타입을 나타낸다. 그 아래의 정보 대부분은 인벤토리에서 아이템을 관리하기 위한 것들이다.
아이템의 스켈레탈 메쉬와 스태틱 메쉬를 구분한 이유는, 전자는 캐릭터에 장착하기 위한 용도이며 후자는 레벨에 배치될 때 사용하기 위한 용도이기 때문이다.
아이템은 캐릭터와 마찬가지로 기본 스탯과 데미지 스탯을 소유한다. 아이템을 사용하면 캐릭터의 스탯 컴포넌트에서 구조체간 연산이 진행된다.
데이터베이스 서브시스템
위와 같은 아이템 구조체는 UIVItemDatabase
라는 UDataAsset
기반 클래스에서 종합적으로 관리한다. 구조는 아래와 같다.
// IVItemDatabase.h
UCLASS()
class IVAN_API UIVItemDatabase : public UDataAsset
{
GENERATED_BODY()
public:
/* 데이터베이스에 저장된 아이템의 세부 정보들 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Items")
TArray<FItemBaseInfo> Items;
/* 아이템 ID와 생성할 액터 블루프린트를 매칭해둔 목록. 아이템 스폰에 사용된다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Items")
TMap<FName, TSubclassOf<AIVItemBase>> ItemActorMap;
/* 아이템 ID를 기반으로 세부 정보를 반환 */
FItemBaseInfo* GetItemBaseInfo(const FName& ItemID);
};
아이템 구조체 목록인 Items
이외에도 ItemActorMap
을 멤버로 가진다. ItemActorMap
는 ItemID와 액터 블루프린트 클래스를 매핑하여, 아이템을 레벨에 스폰할 때 사용한다.
에디터에서 UIVItemDatabase
를 상속받은 데이터 에셋 블루프린트를 하나 만들고, 아이템의 정보를 입력한 모습이다.
게임이 실행되는동안, 개별 아이템이나 인벤토리 등의 다양한 요소들이 해당 데이터베이스에 접근해야한다. 그렇기에 UGameInstanceSubsystem
을 상속받은 UIVDatabaseSubsystem
에서 데이터베이스를 관리한다. 게임이 시작될 때 데이터베이스 에셋을 로드하고, 종료될 때 까지 자원을 해제하지 않는다.
// IVDatabaseSubsystem.cpp
void UIVDatabaseSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// 아이템 데이터베이스 로드
const FString ItemDatabasePath = TEXT("/Script/IVAN.IVItemDatabase'/Game/Items/DA_ItemDatabase.DA_ItemDatabase'");
ItemDatabase = LoadObject<UIVItemDatabase>(nullptr, *ItemDatabasePath);
}
아래와 같이 게임 인스턴스 서브시스템에 접근하여 본 데이터베이스를 이용할 수 있다.
// IVInventoryComponent.cpp
void UIVInventoryComponent::BeginPlay()
{
...
// 아이템 정보는 데이터베이스 서브시스템에서 생성 및 관리한다
UIVDatabaseSubsystem* DatabaseSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<UIVDatabaseSubsystem>();
if (DatabaseSubsystem)
{
ItemDatabase = DatabaseSubsystem->GetItemDatabase();
}
...
}
이 방식의 장점은 데이터를 한 곳에서 관리하기 편하다는 것 이외에도 공용 아이템 액터를 사용할 수 있다는 것이다. 즉, 아이템별로 개별 블루프린트를 만들 필요 없이 공용 블루프린트에 정보만 주입하여 사용하면 된다. 소모품과 방어구는 이런 방식을 사용하지만 무기는 예외적으로 개별 블루프린트를 가지는데, 이는 무기별 공격 범위와 애니메이션이 다르기 때문이다.
아이템은 어떻게 사용되는가?
아이템은 게임 시작부터 레벨에 배치되어있는 아이템과 동적으로 스폰되는 아이템으로 구분할 수 있다. 이는 bIsPlacedInWorld
변수로 확인 가능하다.
레벨에 배치된 아이템은 BeginPlay()
에서 초기화를 진행한다. 데이터베이스에 접근하여 ItemID 기반으로 정보를 가져오고, 이를 바탕으로 ItemInfo
멤버를 갱신한다. 추가적으로 상호작용 활성화 여부도 결정된다.
이렇게 레벨에 놓인 아이템과 플레이어가 상호작용하면 인벤토리에 아이템이 추가되고, 레벨에서 액터가 삭제된다. 반대로 무기를 장착하거나 아이템을 버리는 경우에는 아이템을 스폰한다. 무기는 추가로 물리 연산을 중단하고 캐릭터의 소켓에 부착된다.