게임 개발 (언리얼 엔진)

UE4 생존게임 제작 - 12 (인벤토리 UI, 아이템 및 아이템 사용 효과 제작)

언린이 2020. 11. 12. 15:35

1. 인벤토리 UMG 생성

 

List View를 이용하여 인벤토리를 제작해볼 것입니다.

List View의 UI를 만들때에는 두개의 UMG가 필요한데, 목록을 가지고 있을 List View UMG와 목록 안에 항목들을 표현할 UMG가 필요합니다.

그리고 인벤토리에 들어갈 항목들의 데이터를 가지고 있을 UObject 타입의 클래스 또한 필요합니다.

 

 

 

 

일단 두개의 UMG와 데이터를 담고 있을 클래스를 만들어주었습니다.

 

 

 

InventoryItem 위젯 블루프린트의 클래스 세팅에서 구현된 인터페이스에 User Object List Entry라는 것을 추가해줘야 합니다. 이걸 추가해줘야 Inventory 위젯 블루프린트에서 InventoryItem 위젯 블루프린트를 인식하고 불러올 수 있습니다.

 

 

2. InventoryItem 위젯 블루프린트 제작

 

 

InventoryItem 위젯 블루프린트를 꾸며보았습니다.

배경 이미지, 아이템 아이콘, 아이템 이름, 아이템 수량의 정보를 나타내도록 이미지와 텍스트 박스를 추가해주었습니다. 그리고 C++ 클래스에서 이미지와 텍스트 박스에 접근할 수 있도록 변수인지를 제거해주었습니다.

 

 

 

InventoryItem 위젯 블루프린트의 모습입니다.

 

 

3. Inventory 위젯 블루프린트 제작

 

 

Inventory 위젯 블루프린트는 배경 이미지, List View(여기에 InventoryItem 위젯 블루프린트를 불러올 것임), 텍스트 박스를 추가하여 꾸며주었습니다.

 

 

 

그리고 나서 List View의 List Entries에 InventoryItem 위젯 블루프린트를 설정해주었습니다.

 

 

 

Inventory 위젯 블루프린트의 모습입니다.

 

 

4. 인벤토리 키 입력 추가

 

 

프로젝트 세팅에서 인벤토리를 열기 위한 키 입력을 추가하였습니다.

 

 

PlayerInputComponent->BindAction(TEXT("OpenInventory"), EInputEvent::IE_Pressed, this, &APlayerSurvivor::OpenInventory);

 

그리고 나서 플레이어의 입력을 감지하는 SetupPlayerInputComponent 함수에서 액션 매핑을 통해 실행될 함수를 등록해주었습니다.

 

 

// 인벤토리를 여는 함수
void APlayerSurvivor::OpenInventory()
{	
	if (Inventory->IsVisible())
		Inventory->VisibleInventory(ESlateVisibility::Collapsed);
	else
		Inventory->VisibleInventory(ESlateVisibility::Visible);
}

 

키 입력이 들어오면 인벤토리의 Visibility를 확인하여 상태 설정을 해주었습니다.

(인벤토리 상태: Collapsed = 화면에 안보임, Visible = 화면에 보임)

 

 

5. Inventory Item Data 제작

 

protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	int32 Index;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	int32 ItemNumber;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	int32 ItemCount;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	FString strName;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	UTexture2D* IconTexture;

 

Inventory Item Data는 실제로 아이템에 대한 정보를 저장하는 클래스입니다.

아이템의 인덱스, 개수, 이름, 텍스쳐 정보를 가지고 있게 하였습니다.

 

 

	// List에 데이터 추가
	UInventoryItemData* RawMeat = NewObject<UInventoryItemData>(this, UInventoryItemData::StaticClass());
	RawMeat->SetItemIndex(0);
	RawMeat->SetItemCount(0);
	RawMeat->SetItemName(TEXT("Raw Meat"));
	RawMeat->SetIconTexture(TEXT("Texture2D'/Game/CraftResourcesIcons/Textures/Tex_meat_03.Tex_meat_03'"));
	List->AddItem(RawMeat);

 

Inventory 클래스에서 Inventory Item Data 타입의 오브젝트를 생성하고 각각의 정보들을 설정해 준 다음에 List에 추가해주었습니다.

 

 

// BlueprintCallable로 SetData 함수를 선언하여
// 블루프린트에서 Data를 세팅할 수 있게 해준다
void UInventoryItem::SetData(class UInventoryItemData* Data)
{
	Index = Data->GetItemIndex();
	SetItemCount(Data->GetItemCount());
	SetItemName(Data->GetItemName());
	SetIconTexture(Data->GetIconTexture());
}

 

Inventory Item 클래스에서 BlueprintCallable로 SetData 함수를 만들어서 블루프린트에서 Data를 세팅할 수 있게 해주었습니다.

 

 

 

마지막으로 블루프린트에서 SetData 함수를 사용하여 List에 아이템을 넣어주었습니다.

 

 

 

인벤토리를 열어보면 아이템이 세팅된 모습을 확인할 수 있습니다.

 

 

6. 몬스터 아이템 드랍 제작

 

protected:
	UPROPERTY(Category = Mesh, VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	UStaticMeshComponent* Mesh;

	UPROPERTY(Category = Collision, VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	UBoxComponent* CollisionBox;

	int32 ItemIndex;

	class UUISurvivorState* SurvivorStateWidget;

	class UInventory* Inventory;

 

액터를 상속받아 드랍 아이템 클래스를 제작하였습니다.

클래스에 메시와 플레이어와의 충돌을 감지할 박스 컴포넌트를 선언해주었고 플레이어와 충돌시 아이템이 인벤토리로 들어가야 하기 때문에 인벤토리 또한 선언해주었습니다.

 

 

 

메시와 박스 컴포넌트는 블루프린트를 사용하여 지정해주었습니다.

 

 

	CollisionBox->OnComponentBeginOverlap.AddDynamic(this, &ADropItem::ItemBeginOverlap);

 

// Overlap이 시작할 때 호출되는 함수
void ADropItem::ItemBeginOverlap(UPrimitiveComponent* OverlappedComponent,
	AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
	bool bFromSweep, const FHitResult& SweepResult)
{
	// List에 아이템 세팅
	UListView* List = Inventory->GetList();
	UInventoryItemData* Data = Cast<UInventoryItemData>(List->GetItemAt(ItemIndex));
	Data->SetItemCount(Data->GetItemCount() + 1);
	
	// List 업데이트
	List->RegenerateAllEntries();

	// 액터 제거
	this->Destroy();
}

 

박스 컴포넌트와 플레이어 사이에 Overlap이 발생하면 실행될 델리게이트에 함수를 등록해주었습니다.

해당 함수에서는 List에 아이템을 세팅한 후, List를 업데이트해주고 자기 자신을 월드에서 제거하도록 하였습니다.

 

 

void AAnimal::Death()
{
	// 아이템이 생성될 위치 = 몬스터의 위치
	FVector vPos = this->GetActorLocation();

	// 스폰 파라미터를 설정한다
	FActorSpawnParameters params;
	params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

	// 월드에 액터를 배치한다
	ADropItem* Item = GetWorld()->SpawnActor<ADropItem>(DropItemClass, vPos, GetActorRotation(), params);

	// 몬스터마다 드랍하는 아이템이 다르기 때문에 Index 세팅
	if (IsValid(Item))
		Item->SetItemIndex(AnimalItem.iItemIndex);

	if (SpawnPoint)
		SpawnPoint->Respawn();

	Destroy();
}

 

만들어준 아이템 액터를 몬스터가 죽을 때, 드랍되게 하기 위해서 몬스터의 Death 함수에서 SpawnActor 함수를 사용하여 드랍 아이템을 월드에 생성해주었습니다. 그리고 몬스터 각각마다 드랍하는 아이템이 다르기 때문에 인덱스를 넘겨주어서 어떤 몬스터가 드랍한 아이템인지 지정해주었습니다.

 

 

 

몬스터의 위치에 아이템을 스폰하면 몬스터의 Root는 공중에 떠있기 때문에 아이템 또한 공중에 떠있습니다. 그래서 드랍 아이템의 블루프린트 클래스에서 물리를 적용해주었습니다.

 

 

 

이제 몬스터가 죽으면 아이템이 드랍되고 땅에 떨어지게 됩니다.

 

 

 

그리고 아이템을 먹으면 인벤토리가 업데이트됩니다.

 

 

7. 아이템 사용 효과 제작

 

	// List의 항목 클릭시 실행될 델리게이트에 함수 등록
	List->OnItemDoubleClicked().AddUObject(this, &UInventory::ItemClick);

 

void UInventory::ItemClick(UObject* obj)
{
	// 클릭된 아이템을 가져온다
	UInventoryItemData* pData = Cast<UInventoryItemData>(obj);

	if (IsValid(pData))
	{
		// 아이템의 인덱스를 가져온다
		int32 ItemIndex = pData->GetItemIndex();

		// 플레이어의 함수를 호출하기 위해 컨트롤러를 가져온다
		ASurvivorController* SurvivorController = Cast<ASurvivorController>(GetWorld()->GetFirstPlayerController());

		if (IsValid(SurvivorController))
		{
			// 플레이어를 가져온다
			APlayerSurvivor* Survivor = Cast<APlayerSurvivor>(SurvivorController->GetCharacter());

			if (IsValid(Survivor))
			{
				// 아이템 사용에 대한 작업을 시행하는 함수 호출
				Survivor->SetPlayerStateUpdate(ItemIndex);
			}
		}
	}
}

 

List의 항목 클릭시 실행될 델리게이트에 함수를 등록해주었습니다. 등록될 함수는 UObject 포인터 타입을 인자로 받는 함수여야합니다. UObject 포인터 타입으로 리스트에서 클릭된 항목을 가져오기 때문입니다.

그리고 해당 함수에서 플레이어에 정의해둔 아이템 사용에 대한 작업을 시행하는 함수를 호출하였습니다.

함수의 인자로는 어떤 아이템인지 알아야 하기 때문에 ItemIndex를 넘겨주었습니다.

 

 

// 아이템 사용시 플레이어의 상태를 업데이트하는 함수
void APlayerSurvivor::SetPlayerStateUpdate(int32 Index)
{
	// 넘겨받은 인덱스에 해당하는 아이템을 가져온다
	UInventoryItemData* Data = Cast<UInventoryItemData>(List->GetItemAt(Index));

	switch (Index)
	{
	case 0:
		// 생고기
		if (Data->GetItemCount() > 0)
		{
			SurvivorState.iHP += 10;
			if (SurvivorState.iHP >= 100)
				SurvivorState.iHP = 100;

			Data->SetItemCount(Data->GetItemCount() - 1);
		}
		break;
	case 1:
		// 구운고기
		if (Data->GetItemCount() > 0)
		{
			SurvivorState.iHP += 30;
			if (SurvivorState.iHP >= 100)
				SurvivorState.iHP = 100;

			Data->SetItemCount(Data->GetItemCount() - 1);
		}
		break;
	case 13:
		// Armor Up
		if (Data->GetItemCount() > 0)
		{
			SurvivorState.fArmor += 10.f;

			// 방어력은 20을 넘지 못한다
			if (SurvivorState.fArmor > 20.f)
				SurvivorState.fArmor = 20.f;

			Data->SetItemCount(Data->GetItemCount() - 1);
		}
		break;
	case 14:
		// Attack Up
		if (Data->GetItemCount() > 0)
		{
			SurvivorState.fAttack += 10.f;

			// 공격력은 30을 넘지 못한다
			if (SurvivorState.fAttack > 30.f)
				SurvivorState.fAttack = 30.f;

			Data->SetItemCount(Data->GetItemCount() - 1);
		}
		break;
	}

	// List 업데이트
	List->RegenerateAllEntries();

	// 생고기나 구운고기를 먹으면 플레이어의 HP가 달라지기 때문에 SetHP 함수 호출
	if (IsValid(SurvivorStateWidget))
		SurvivorStateWidget->SetHP(SurvivorState.iHP / (float)SurvivorState.iHPMax);
}

 

switch 문을 사용하여 각 아이템 사용에 대한 플레이어의 상태를 업데이트해주었습니다.

 

 

 

Armor Up 아이템 사용전 플레이어의 Armor는 10입니다.

 

 

 

Armor Up 아이템 사용시 플레이어의 Armor가 20으로 세팅되는 모습을 확인할 수 있습니다.

 

 

다음에 해야할 일

 

아이템 제작창 제작