[UE5 C++] 아이템 & 인벤토리 시스템 - 3

LeeTaes·2024년 5월 18일
0

[UE_Project] MysticMaze

목록 보기
14/17

언리얼 엔진을 사용한 RPG 프로젝트 만들기

  • 인벤토리 및 슬롯 위젯 제작하기

위젯 클래스 설계 및 간단 설명

MMCustomWidget Class

  • UUserWidget은 플레이어를 저장하기 위한 변수를 제공하지 않습니다.
  • 플레이어를 저장하기 위한 변수와 함수만을 가진 "커스텀 위젯" 클래스를 통해 플레이어에 접근하도록 하겠습니다.
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MMCustomWidget.generated.h"

/**
 * 
 */
UCLASS()
class MYSTICMAZE_API UMMCustomWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	FORCEINLINE void SetOwningActor(AActor* NewOwner) { OwningActor = NewOwner; }

protected:
	// 현재 위젯을 소유하고 있는 액터 저장용 변수
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Actor")
	TObjectPtr<AActor> OwningActor;
};

MMSlot Class

  • 인벤토리, 퀵슬롯 등 앞으로 제작할 위젯에서 사용될 슬롯 클래스입니다.
  • 모든 곳에서 같이 사용할 수 있도록 제작할 예정입니다.

MMToolTip

  • 슬롯 위에 마우스 커서를 올렸을 경우 출력되는 툴팁을 구현하기 위한 클래스입니다.

슬롯 구현하기

  • 슬롯은 해당 인벤토리 아이템의 이미지와 수량을 보여줘야 합니다.
  • 슬롯 인덱스와 타입을 지정하여 구현해주도록 하겠습니다.

우선 슬롯에서 앞으로 사용할 타입을 추가해주도록 하겠습니다.

타입에 따라 슬롯 업데이트시 참조할 인벤토리를 정해 해당 인덱스에 접근하여 슬롯을 업데이트하도록 구현할 예정입니다.


#pragma once

#include "CoreMinimal.h"
#include "MMEnums.generated.h"

...

UENUM(BlueprintType)
enum class ESlotType : uint8
{
    ST_None,
    ST_InventoryEquipment,  // 인벤토리(장비)
    ST_InventoryConsumable, // 인벤토리(소비)
    ST_InventoryOther,      // 인벤토리(기타)
	...
};

간단히 이미지와 수량, 인덱스와 타입을 저장하는 슬롯 클래스를 생성해주도록 합니다.

슬롯의 소유 액터를 통해 인벤토리에 접근해야 하므로 MMCustomWidget 클래스를 상속받아 제작해주도록 하겠습니다.

#pragma once

#include "CoreMinimal.h"
#include "UI/MMCustomWidget.h"
#include "GameData/MMEnums.h"
#include "MMSlot.generated.h"

/**
 * 
 */
UCLASS()
class MYSTICMAZE_API UMMSlot : public UMMCustomWidget
{
	GENERATED_BODY()

protected:
	virtual void NativeConstruct() override;
	
public:
	// 슬롯 초기화 함수
	void Init();
    // 슬롯의 타입을 지정해주기 위한 함수
	void SetType(ESlotType Type);
    // 슬롯을 업데이트해주기 위한 함수
	void UpdateSlot();

public:
	// 현재 슬롯의 타입
	UPROPERTY(VisibleAnywhere, Category = "Slot")
	ESlotType SlotType;

	// 슬롯에 지정될 이미지
	UPROPERTY(VisibleAnywhere, Category = "Slot", meta = (BindWidget = "true"))
	TObjectPtr<class UImage> IMG_Item;

	// 슬롯에 지정될 아이템의 수량
	UPROPERTY(VisibleAnywhere, Category = "Slot", meta = (BindWidget = "true"))
	TObjectPtr<class UTextBlock> TXT_Quantity;

	// 현재 슬롯의 인덱스
	UPROPERTY(EditAnywhere, Category = "Slot")
	int32 SlotIndex;

protected:
	// 개별 슬롯 업데이트 함수
	void UpdateEquipmentSlot();
	void UpdateConsumableSlot();
	void UpdateOtherSlot();
    
    // 빈 칸에 적용하기 위한 투명 텍스쳐
	UPROPERTY(EditAnywhere, Category = "Slot")
	TObjectPtr<class UTexture2D> DefaultTexture;
};

MMSlotClass를 기반으로 한 Widget을 생성해 디자인해주도록 하겠습니다.

이제는 플레이어로부터 인벤토리 컴포넌트를 가져와서 업데이트하는 로직을 추가해야 합니다.

인터페이스를 통해 인벤토리 컴포넌트만을 받아오는 클래스를 제작해주도록 하겠습니다.

MMInventoryInterface Class

  • 인벤토리 컴포넌트를 반환하는 클래스입니다.
  • 플레이어에 상속받게 하여 인벤토리를 반환할 수 있도록 해줍니다.
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "MMInventoryInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UMMInventoryInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class MYSTICMAZE_API IMMInventoryInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual class UMMInventoryComponent* GetInventoryComponent() = 0;
};

생성한 인벤토리 인터페이스를 사용해 슬롯 내부 업데이트 함수들을 구현해주도록 하겠습니다.

타입에 따라 업데이트 함수가 달라져야 하므로 일방적으로 switch문을 사용해 제작할 수 있지만 델리게이트를 사용하는 버전으로 구현해보도록 하겠습니다.

MMSlot Class

// MMSlot Header

// 업데이트 델리게이트 선언
DECLARE_DELEGATE(FOnUpdateSlotDelegate);

// 래퍼 구조체를 선언
USTRUCT(BlueprintType)
struct FUpdateSlotDelegateWrapper
{
	GENERATED_BODY()
	
	FUpdateSlotDelegateWrapper() { }
	FUpdateSlotDelegateWrapper(const FOnUpdateSlotDelegate& InSlotDelegate) : SlotDelegate(InSlotDelegate) { }

	FOnUpdateSlotDelegate SlotDelegate;
};

...

protected:
	// <슬롯 타입, 래퍼 구조체> 형식의 Map 선언
	UPROPERTY()
	TMap<ESlotType, FUpdateSlotDelegateWrapper> SlotUpdateActions;
// MMSlot Cpp

void UMMSlot::Init()
{
	// 개별 함수를 연동해 맵에 저장해주도록 합니다.
	SlotUpdateActions.Add(ESlotType::ST_InventoryEquipment, FUpdateSlotDelegateWrapper(FOnUpdateSlotDelegate::CreateUObject(this, &UMMSlot::UpdateEquipmentSlot)));
	SlotUpdateActions.Add(ESlotType::ST_InventoryConsumable, FUpdateSlotDelegateWrapper(FOnUpdateSlotDelegate::CreateUObject(this, &UMMSlot::UpdateConsumableSlot)));
	SlotUpdateActions.Add(ESlotType::ST_InventoryOther, FUpdateSlotDelegateWrapper(FOnUpdateSlotDelegate::CreateUObject(this, &UMMSlot::UpdateOtherSlot)));

	// 초기화 시 업데이트를 진행합니다.
	UpdateSlot();
}

void UMMSlot::UpdateSlot()
{
	// 슬롯 타입에 따라 실행되는 함수 호출
	SlotUpdateActions[SlotType].SlotDelegate.ExecuteIfBound();
}

void UMMSlot::UpdateEquipmentSlot()
{
	IMMInventoryInterface* InvPlayer = Cast<IMMInventoryInterface>(OwningActor);

	if (InvPlayer)
	{
		// 인벤토리(Equipment)를 가져옵니다.
		TArray<UMMInventoryItem*> InventoryItems = InvPlayer->GetInventoryComponent()->GetEquipmentItems();
		
		// 현재 Slot의 인덱스가 유효한지 체크합니다.
		if (InventoryItems.IsValidIndex(SlotIndex))
		{
			// 해당 슬롯에 아이템이 존재하는지 확인합니다.
			if (IsValid(InventoryItems[SlotIndex]))
			{
				// 존재하는 경우 아이템의 텍스쳐와 수량을 반영해주도록 합니다. (장비는 수량 표시 X)
				IMG_Item->SetBrushFromTexture(InventoryItems[SlotIndex]->ItemData->ItemTexture);
				TXT_Quantity->SetText(FText::FromString(TEXT("")));
			}
			else
			{
				// 존재하지 않는 경우 빈 칸으로 표시합니다.
				IMG_Item->SetBrushFromTexture(DefaultTexture);
				TXT_Quantity->SetText(FText::FromString(TEXT("")));
			}
		}
	}
}

void UMMSlot::UpdateConsumableSlot()
{
	IMMInventoryInterface* InvPlayer = Cast<IMMInventoryInterface>(OwningActor);

	if (InvPlayer)
	{
		// 인벤토리(Consumable)를 가져옵니다.
		TArray<UMMInventoryItem*> InventoryItems = InvPlayer->GetInventoryComponent()->GetConsumableItems();

		// 현재 Slot의 인덱스가 유효한지 체크합니다.
		if (InventoryItems.IsValidIndex(SlotIndex))
		{
			// 해당 슬롯에 아이템이 존재하는지 확인합니다.
			if (IsValid(InventoryItems[SlotIndex]))
			{
				// 존재하는 경우 아이템의 텍스쳐와 수량을 반영해주도록 합니다. (소비는 수량 표시 O)
				IMG_Item->SetBrushFromTexture(InventoryItems[SlotIndex]->ItemData->ItemTexture);
				TXT_Quantity->SetText(FText::FromString(FString::Printf(TEXT("%d"), InventoryItems[SlotIndex]->ItemQuantity)));
			}
			else
			{
				// 존재하지 않는 경우 빈 칸으로 표시합니다.
				IMG_Item->SetBrushFromTexture(DefaultTexture);
				TXT_Quantity->SetText(FText::FromString(TEXT("")));
			}
		}
	}
}

void UMMSlot::UpdateOtherSlot()
{
	IMMInventoryInterface* InvPlayer = Cast<IMMInventoryInterface>(OwningActor);

	if (InvPlayer)
	{
		// 인벤토리(Consumable)를 가져옵니다.
		TArray<UMMInventoryItem*> InventoryItems = InvPlayer->GetInventoryComponent()->GetOtherItems();

		// 현재 Slot의 인덱스가 유효한지 체크합니다.
		if (InventoryItems.IsValidIndex(SlotIndex))
		{
			// 해당 슬롯에 아이템이 존재하는지 확인합니다.
			if (IsValid(InventoryItems[SlotIndex]))
			{
				// 존재하는 경우 아이템의 텍스쳐와 수량을 반영해주도록 합니다. (기타는 수량 표시 O)
				IMG_Item->SetBrushFromTexture(InventoryItems[SlotIndex]->ItemData->ItemTexture);
				TXT_Quantity->SetText(FText::FromString(FString::Printf(TEXT("%d"), InventoryItems[SlotIndex]->ItemQuantity)));
			}
			else
			{
				// 존재하지 않는 경우 빈 칸으로 표시합니다.
				IMG_Item->SetBrushFromTexture(DefaultTexture);
				TXT_Quantity->SetText(FText::FromString(TEXT("")));
			}
		}
	}
}

인벤토리 위젯 구현하기

  • 인벤토리 컴포넌트의 내용을 출력하기 위한 인벤토리 위젯을 제작해보도록 하겠습니다.

이번에는 위젯의 디자인을 우선적으로 해주도록 하겠습니다.

Inventory라고 적혀있는 위젯의 틀은 Button으로 추후 위젯 이동을 구현할 예정입니다.

내부 Equipments, Consumables, Others 또한 마찮가지로 Button으로 클릭 시 해당 인벤토리의 내용으로 슬롯을 업데이트 해줄 예정입니다.

추가적으로 아래 슬롯들의 구조는 "스크롤박스[가로박스[슬롯1, 슬롯2...]]]" 형식으로 제작하였습니다.

모든 슬롯의 인덱스를 지정해주도록 합니다.
저는 30칸을 기준으로 제작할 예정이므로 인덱스는 0 ~ 29로 설정하였습니다.

MMInventoryWidget Class

  • 인벤토리 내부 내용들을 출력하기 위해 슬롯들을 관리하는 역할을 하는 위젯입니다.
  • 인벤토리에 접근해야 하므로 소유 액터의 정보를 얻어오기 위해 "커스텀 위젯"을 상속받아 구현하였습니다.
// MMInventoryWidget Header

#pragma once

#include "CoreMinimal.h"
#include "UI/MMCustomWidget.h"
#include "GameData/MMEnums.h"
#include "MMInventoryWidget.generated.h"

/**
 * 
 */
UCLASS()
class MYSTICMAZE_API UMMInventoryWidget : public UMMCustomWidget
{
	GENERATED_BODY()
	
protected:
	virtual void NativeConstruct() override;

public:
	// 인벤토리 초기화 함수
	void Init();
    // 인벤토리 아이템 슬롯 업데이트 함수
	void UpdateInventorySlot();
    // 인벤토리 골드 업데이트 함수
	void UpdateInventoryGold();

private:
	UFUNCTION()
	void SetEquipmentType();

	UFUNCTION()
	void SetConsumableType();

	UFUNCTION()
	void SetOtherType();

	UFUNCTION()
	void SortItem();

// Main
public:
	UPROPERTY(meta = (BindWidget = "true"))
	TObjectPtr<class UButton> BTN_MainButton;

// Header
public:
	UPROPERTY(meta = (BindWidget = "true"))
	TObjectPtr<class UButton> BTN_Equipment;

	UPROPERTY(meta = (BindWidget = "true"))
	TObjectPtr<class UButton> BTN_Consumable;

	UPROPERTY(meta = (BindWidget = "true"))
	TObjectPtr<class UButton> BTN_Other;

// Tail
public:
	UPROPERTY(meta = (BindWidget = "true"))
	TObjectPtr<class UButton> BTN_SortItem;

	UPROPERTY(meta = (BindWidget = "true"))
	TObjectPtr<class UTextBlock> TXT_Gold;

private:
	// 현재 인벤토리의 타입을 지정하기 위한 함수
	void SetType(ESlotType Type);

	// 슬롯들을 저장하기 위한 배열
	UPROPERTY(VisibleAnywhere, Category = "Inventory")
	TArray<TObjectPtr<class UMMSlot>> Slots;

	// 현재 인벤토리의 타입 (장비 or 소비 or 기타)
	ESlotType InventorySlotType;
};
void UMMInventoryWidget::Init()
{
	// 기본 타입을 장비로 설정합니다.
	SetType(ESlotType::ST_InventoryEquipment);

	// 버튼 클릭 이벤트와 함수를 연동시켜 줍니다.
	if (BTN_Equipment)
	{
		BTN_Equipment->OnClicked.AddDynamic(this, &UMMInventoryWidget::SetEquipmentType);
	}

	if (BTN_Consumable)
	{
		BTN_Consumable->OnClicked.AddDynamic(this, &UMMInventoryWidget::SetConsumableType);
	}

	if (BTN_Other)
	{
		BTN_Other->OnClicked.AddDynamic(this, &UMMInventoryWidget::SetOtherType);
	}

	if (BTN_SortItem)
	{
		BTN_SortItem->OnClicked.AddDynamic(this, &UMMInventoryWidget::SortItem);
	}

	// 슬롯을 저장하기 위한 배열을 초기화합니다. (30칸)
	Slots.Init(nullptr, 30);

	TArray<UWidget*> Widgets;
    // 모든 위젯들을 가져와 Widgets 배열에 저장합니다.
	WidgetTree->GetAllWidgets(Widgets);

	// 배열을 순회하며 Slot을 찾아주도록 합니다.
	for (UWidget* Widget : Widgets)
	{
		UMMSlot* InvSlot = Cast<UMMSlot>(Widget);
		if (InvSlot)
		{
        	// 슬롯의 소유주를 현재 소유주로 지정합니다.
			InvSlot->SetOwningActor(OwningActor);
            // 슬롯의 타입을 현재 인벤토리의 타입으로 지정합니다.
			InvSlot->SetType(InventorySlotType);
            // 슬롯을 초기화합니다.
			InvSlot->Init();
            // 해당 슬롯의 인덱스 위치에 슬롯을 저장합니다.
			Slots[InvSlot->SlotIndex] = InvSlot;
		}
	}
}

void UMMInventoryWidget::UpdateInventorySlot()
{
	// 슬롯을 현재 인벤토리 타입으로 업데이트 합니다.
	for (const auto& InvSlot : Slots)
	{
		InvSlot->SetType(InventorySlotType);
		InvSlot->UpdateSlot();
	}
}

void UMMInventoryWidget::UpdateInventoryGold()
{
	// 골드량을 인벤토리 컴포넌트로부터 받아와 재설정합니다.
	IMMInventoryInterface* InvPlayer = Cast<IMMInventoryInterface>(OwningActor);
	if (InvPlayer)
	{
		TXT_Gold->SetText(FText::FromString(FString::Printf(TEXT("%d"), InvPlayer->GetInventoryComponent()->GetCurrentGold())));
	}
}

void UMMInventoryWidget::SetEquipmentType()
{
	SetType(ESlotType::ST_InventoryEquipment);
}

void UMMInventoryWidget::SetConsumableType()
{
	SetType(ESlotType::ST_InventoryConsumable);
}

void UMMInventoryWidget::SetOtherType()
{
	SetType(ESlotType::ST_InventoryOther);
}

void UMMInventoryWidget::SortItem()
{
	// 인벤토리 컴포넌트 내부 아이템 정렬 함수를 호출합니다.
	IMMInventoryInterface* InvPlayer = Cast<IMMInventoryInterface>(OwningActor);
	if (InvPlayer)
	{
		InvPlayer->GetInventoryComponent()->SortItem(InventorySlotType);
	}
}

void UMMInventoryWidget::SetType(ESlotType Type)
{
	// 현재 인벤토리의 타입을 변경합니다.
	InventorySlotType = Type;
	// 타입이 변경되었으므로 슬롯을 다시 업데이트 해줍니다.
	UpdateInventorySlot();
}

결과 확인하기

  • 테스트로 레벨 블루프린트에서 게임 시작과 동시에 인벤토리 위젯을 출력해 결과를 확인해보도록 하겠습니다.


인벤토리 컴포넌트에서 초기화했던 내용이 적용되는 것을 확인할 수 있으며 아이템 정렬 기능이 제대로 동작하는 것을 확인할 수 있습니다.

profile
클라이언트 프로그래머 지망생

0개의 댓글