![[Unreal Engine C++] 상호작용하여 Slot에 아이템을 추가하기](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZfDt0%2FbtsMDtN4UAI%2FjE1vi8dXz3CKHmF11DxDVk%2Fimg.png)
인터넷에 검색하면 나오는 Slot들은 모두 인벤토리를 활용한다.
필자의 프로젝트에서는 인벤토리를 사용하지 않기 때문에 그 기능을 제외하고 Slot 기능을 제작해보자.
아이템을 상호작용 하거나 얻게 될 시에 내 캐릭터가 장착하게 된다.
하지만, 인벤토리 기능이 없기 때문에 이를 직접 확인하고 적용해줘야 한다.
아이템테이블 및 Slot 생성
일단 Slot에 장착되는 것들은 모두 아이템이기 때문에 이 정보들을 담고있는 데이터베이스가 필요하다.
// ItemDataStructs.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "ItemDataStructs.generated.h"
UENUM()
enum class EItemType : uint8 // 아이템 타입
{
Weapon UMETA(DisplayName = "Weapon"),
Consumable UMETA(DisplayName = "Consumable"),
Module UMETA(DisplayName = "Module")
};
USTRUCT()
struct FItemStatistics // 아이템 능력
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere)
float DamageValue;
UPROPERTY(EditAnywhere)
float RestorationValue;
};
USTRUCT()
struct FItemTextData // 문자 데이터
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere)
FText Name;
UPROPERTY(EditAnywhere)
FText Description;
UPROPERTY(EditAnywhere)
FText InteractionText;
UPROPERTY(EditAnywhere)
FText UsageText;
};
USTRUCT()
struct FItemNumericData // 가질 수 있는 양에 대한 정보
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere)
int32 MaxStackSize;
UPROPERTY(EditAnywhere)
bool bIsStackable;
};
USTRUCT()
struct FItemAssetData // 에셋 정보
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere)
UTexture2D* Icon;
UPROPERTY(EditAnywhere)
UStaticMesh* Mesh;
};
USTRUCT()
struct FItemData : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, Category = "Item Data")
FName ID;
UPROPERTY(EditAnywhere, Category = "Item Data")
EItemType ItemType;
UPROPERTY(EditAnywhere, Category = "Item Data")
FItemStatistics ItemStatistics;
UPROPERTY(EditAnywhere, Category = "Item Data")
FItemTextData ItemTextData;
UPROPERTY(EditAnywhere, Category = "Item Data")
FItemNumericData ItemNumericData;
UPROPERTY(EditAnywhere, Category = "Item Data")
FItemAssetData ItemAssetData;
};
아이템에 들어갈만한 요소들을 선언해주고, 이 데이터 목록을 이용할 것이다.
언리얼에서 마우스 우클릭 > 기타 > 데이터 테이블로 하여 위에 ItemDataStructs를 상속받는 데이터 테이블을 만들어준다.
UserWidget을 상속받는 C++ Slot 을 생성하고 이를 상속받는 블루프린트 위젯도 만들어준다.
// Slot.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Slot.generated.h"
/**
*
*/
UCLASS()
class P3_API USlot : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeOnInitialized() override;
void SetSlot(FItemData* ItemData);
void RemoveSlot();
bool GetIsEmpty();
FItemData* GetItemData();
protected:
bool bIsEmpty = true;
private:
class UImage* Icon;
FItemData* CurrentItemData;
};
이 Slot에는 아이템의 아이콘을 보여주는 Icon이 존재한다.
또한 내용을 채우기 위한 함수들도 선언해주었다.
// Slot.cpp
#include "Slot.h"
#include "../Public/Data/ItemDataStructs.h"
#include "Components/Image.h"
void USlot::NativeOnInitialized()
{
Super::NativeOnInitialized();
Icon = Cast<UImage>(GetWidgetFromName(TEXT("Icon")));
}
void USlot::SetSlot(FItemData* ItemData)
{
CurrentItemData = ItemData;
Icon->SetBrushFromTexture(ItemData->ItemAssetData.Icon);
bIsEmpty = false;
}
void USlot::RemoveSlot()
{
Icon->SetBrushFromTexture(nullptr);
bIsEmpty = true;
}
FItemData* USlot::GetItemData()
{
return CurrentItemData;
}
bool USlot::GetIsEmpty()
{
return bIsEmpty;
}
이 슬롯들을 관리하는 SlotPanel도 만들어서, 이 슬롯들을 배치해준다.
// SlotPanel.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SlotPanel.generated.h"
/**
*
*/
UCLASS()
class P3_API USlotPanel : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeOnInitialized() override;
void AddItemIntoSlot(FItemData* ItemData);
void RemoveItemFromSlot(int32 index);
FItemData* GetSlotData(int32 index);
protected:
class USlot* Weapon_1;
class USlot* Weapon_2;
class USlot* Consumable;
class USlot* Module_1;
};
// SlotPanel.cpp
#include "SlotPanel.h"
#include "GBufferInfo.h"
#include "Slot.h"
#include "Data/ItemDataStructs.h"
void USlotPanel::NativeOnInitialized()
{
Super::NativeOnInitialized();
Weapon_1 = Cast<USlot>(GetWidgetFromName(TEXT("Slot_Weapon_1")));
Weapon_2 = Cast<USlot>(GetWidgetFromName(TEXT("Slot_Weapon_2")));
Consumable = Cast<USlot>(GetWidgetFromName(TEXT("Slot_Consumable")));
Module_1 = Cast<USlot>(GetWidgetFromName(TEXT("Slot_Module_1")));
}
void USlotPanel::AddItemIntoSlot(FItemData* ItemData)
{
switch (ItemData->ItemType)
{
case EItemType::Weapon:
if (Weapon_1->GetIsEmpty())
{
Weapon_1->SetSlot(ItemData);
}
else
{
if (Weapon_2->GetIsEmpty())
{
Weapon_2->SetSlot(ItemData);
}
}
break;
case EItemType::Consumable:
if (Consumable->GetIsEmpty())
{
Consumable->SetSlot(ItemData);
}
break;
case EItemType::Module:
if (Module_1->GetIsEmpty())
{
Module_1->SetSlot(ItemData);
}
break;
default:
break;
}
}
void USlotPanel::RemoveItemFromSlot(int32 index)
{
if (index == 1)
{
Weapon_1->RemoveSlot();
}
else if (index == 2)
{
Weapon_2->RemoveSlot();
}
else if (index == 3)
{
Consumable->RemoveSlot();
}
else if (index == 4)
{
Module_1->RemoveSlot();
}
else
{
UE_LOG(LogTemp, Warning, TEXT("This is Empty Item Type"));
}
}
FItemData* USlotPanel::GetSlotData(int32 index)
{
if (index == 1)
{
return Weapon_1->GetItemData();
}
else if (index == 2)
{
return Weapon_2->GetItemData();
}
else if (index == 3)
{
return Consumable->GetItemData();
}
else if (index == 4)
{
return Module_1->GetItemData();
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Invalid Index"));
return nullptr;
}
}
bool USlotPanel::CheckSlotIsEmpty()
{
if (Weapon_1->GetIsEmpty() || Weapon_2->GetIsEmpty())
{
return true;
}
return false;
}
슬롯의 대한 기초적인 준비는 이것으로 끝났다.
상호작용 준비
아이템에 캐릭터가 상호작용 하기 위해서는 상호작용을 하는 주체가 필요하기 때문에 캐릭터에 붙이는 컴포넌트로 구현하였다.
// InteractionComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractionComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class P3_API UInteractionComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UInteractionComponent();
protected:
// Called when the game starts
virtual void BeginPlay() override;
public:
void AddInteractActor(AActor* InteractActor);
void RemoveInteractActor(AActor* InteractActor);
void UpdateCurrentInteractActor();
void Interact();
private:
UPROPERTY()
TArray<AActor*> OverlappingActors;
UPROPERTY()
AActor* CurrentInteractActor;
};
// InteractionComponent.cpp
#include "Character/InteractionComponent.h"
#include "Core/Interface/InteractionInterface.h"
// Sets default values for this component's properties
UInteractionComponent::UInteractionComponent()
{
PrimaryComponentTick.bCanEverTick = false;
CurrentInteractActor = nullptr;
}
// Called when the game starts
void UInteractionComponent::BeginPlay()
{
Super::BeginPlay();
}
void UInteractionComponent::AddInteractActor(AActor* InteractActor)
{
if (!OverlappingActors.Contains(InteractActor)) // 중복 방지
{
OverlappingActors.Add(InteractActor);
UpdateCurrentInteractActor(); // 새 액터 추가 시 현재 상호작용 액터 업데이트
}
}
void UInteractionComponent::RemoveInteractActor(AActor* InteractActor)
{
if (OverlappingActors.Contains(InteractActor))
{
OverlappingActors.Remove(InteractActor);
UpdateCurrentInteractActor(); // 액터 제거 시 현재 상호작용 액터 업데이트
}
}
// 현재 상호작용 액터를 가장 가까운 상호작용 액터로 업데이트
void UInteractionComponent::UpdateCurrentInteractActor()
{
AActor* ClosestInteractActor = nullptr;
float MinDistance = FLT_MAX;
for (AActor* Actor : OverlappingActors)
{
float Distance = FVector::Dist(GetOwner()->GetActorLocation(), Actor->GetActorLocation());
if (Distance < MinDistance)
{
ClosestInteractActor = Actor;
MinDistance = Distance;
}
}
CurrentInteractActor = ClosestInteractActor;
}
void UInteractionComponent::Interact()
{
if (CurrentInteractActor && CurrentInteractActor->Implements<UInteractionInterface>())
{
IInteractionInterface::Execute_Interact(CurrentInteractActor);
}
}
캐릭터가 상호작용 가능한 액터에 도달하였을 때, Distance로 거리를 확인하여 가장 가까운 액터를 상호작용 가능한 액터로 지정하였다.
이는 액터에 붙어있는 SphereComponent로 확인했다.
// PlayerCharacter.cpp
APlayerCharacter::APlayerCharacter()
{
...
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
}
...
캐릭터에 상호작용 컴포넌트를 생성해주어 그 기능을 할 수 있게 했다.
상호작용 전달
상호작용하는 행동은 지금 당장은 아이템밖에 없을 수 있지만, 나중에는 종류가 다양해질 수 있기 때문에
인터페이스를 만들어 공통 함수를 관리하려고 한다.
// InteractionInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "InteractionInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UInteractionInterface : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
class P3_API IInteractionInterface
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void Interact();
};
이 인터페이스를 적용하여 다음과 같은 베이스 아이템을 만들었다.
// InteractableItem.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Core/Interface/InteractionInterface.h"
#include "InteractableItem.generated.h"
UCLASS()
class P3_API AInteractableItem : public AActor, public IInteractionInterface
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AInteractableItem();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
UFUNCTION()
virtual void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
void OnOverlapEnd(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
UPROPERTY(EditAnywhere)
class USphereComponent* InteractionSphere;
public:
virtual void Interact_Implementation() override;
};
위에서 설명했듯 거리 확인 및 상호작용 여부를 확인하기 위해 InteractionSphere를 사용하고 있다.
이 베이스 아이템을 상속받는 액터에 상호작용을 하게 되면 다음과 같은 순서로 작동할 것이다.
1. Character에서 상호작용 키를 누름.
2. Character의 InteractionComponent에서 Interaction()을 인터페이스를 통해 상호작용 액터에 전달.
3. 상호작용 액터(아이템) 에서 아이템에 대한 정보를 SlotPanel에 전달.
4. SlotPanel에서 아이템 종류를 확인하여 슬롯에 장착.
구현
필자의 슬롯은 HUD와 같이 플레이 하는 동안에는 계속 노출될 것이기 때문에 HUD에 SlotPanel을 넣어줄 것이다.
UserWidget을 상속받는 HUD를 만들고 이 안에 SlotPanel을 넣어준다.
HUD는 PlayerController에서 시작 시 생성되게 한다.
이 때, HUD안에 SlotPanel이 존재하기 때문에 접근할 수 있는 함수도 생성해준다.
// PlayerCharacterController.h
...
UCLASS()
class P3_API APlayerCharacterController : public ABasePlayerController
{
GENERATED_BODY()
...
private:
UPROPERTY(EditDefaultsOnly, Category = "Input")
UInputMappingDataAsset* InputMappingDataAsset;
class UUserWidget* HUDWidget;
.
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI")
TSubclassOf<class UUserWidget> HUDWidgetClass;
class USlotPanel* GetSlotPanel();
};
// PlayerCharacterController.cpp
...
APlayerCharacterController::APlayerCharacterController()
{
// HUD Class 불러오기.
static ConstructorHelpers::FClassFinder<UUserWidget> WidgetClass(TEXT("/Script/UMGEditor.WidgetBlueprint'/Game/01_Blueprint/Data/HUD.HUD_C'"));
if (WidgetClass.Succeeded())
{
HUDWidgetClass = WidgetClass.Class;
}
}
void APlayerCharacterController::BeginPlay()
{
Super::BeginPlay();
...
// HUD 생성.
HUDWidget = CreateWidget(this, HUDWidgetClass);
if (HUDWidget)
{
HUDWidget->AddToViewport();
}
}
...
USlotPanel* APlayerCharacterController::GetSlotPanel()
{
if (HUDWidget)
{
USlotPanel* SlotPanel = Cast<USlotPanel>(HUDWidget->GetWidgetFromName(TEXT("SlotPanel")));
return SlotPanel;
}
return nullptr;
}
필자의 프로젝트에서는 아이템을 획득하는 상자가 존재한다.
이 상자를 상호작용하면 아이템 정보를 생성하게 된다.
// LootProp.cpp
#include "Item/LootProp.h"
#include "ItemPanel.h"
#include "Blueprint/UserWidget.h"
#include "Data/ItemDataStructs.h"
ALootProp::ALootProp()
{
...
ItemDataTable = LoadObject<UDataTable>(nullptr, TEXT("/Script/Engine.Texture2D'/Game/01_Blueprint/Data/ItemData.ItemData'"));
}
void ALootProp::BeginPlay()
{
Super::BeginPlay();
if (ItemDataTable)
{
TArray<FItemData*> Items;
for (auto& Row : ItemDataTable->GetRowMap())
{
FItemData* TempItemData = (FItemData*)Row.Value;
if (TempItemData->ItemType == IncludingType)
{
Items.Add(TempItemData);
}
}
// 무작위로 하나 선택
if (Items.Num() > 0)
{
for (int i = 0; i < NumberOfItem; i++)
{
int32 RandomIndex = FMath::RandRange(0, Items.Num() - 1);
FItemData* RandomItem = Items[RandomIndex];
ItemData.Add(RandomItem);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("No weapons found in the data table."));
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to load the data table."));
}
}
...
상자에서는 아이템 데이터를 불러와 값을 보내주는 작업을 수행한다.
GetRowMap()으로 ItemType을 확인하여 상자의 Type과 비교한 후, 상자에 아이템 값을 넣어주었다.
// UItemDescription.cpp
...
void UItemDescription::SelectButtonClicked()
{
APlayerCharacterController* Controller = Cast<APlayerCharacterController>(GetWorld()->GetFirstPlayerController());
if (Controller->GetSlotPanel()->CheckSlotIsEmpty())
{
Controller->GetSlotPanel()->AddItemIntoSlot(CurrentItemData);
}
...
}
...
이를 위에 같은 방법으로 슬롯에 전달해주기만 하면 SlotPanel에서 AddItemIntoSlot 함수를 이용하여 값을 넣어주는 작업을 수행한다.
결과는 다음과 같다.
'Unreal > 공부' 카테고리의 다른 글
[Unreal Engine] 언리얼 엔진의 멀티 플레이 환경 (2) | 2025.03.11 |
---|---|
[Unreal Engine C++] 액터를 원 안의 랜덤 위치에 스폰시키기 (1) | 2024.11.25 |
[Unreal Engine BP] 캐릭터 카메라를 액터에 Close Up 하기 (0) | 2024.09.28 |
[Unreal Engine C++] Dash의 기능에서 개선된 Dodge의 구현 (0) | 2024.08.21 |
[Unreal Engine C++] Widget Blueprint를 이용해 세션 생성 및 접속 기능 만들기 (0) | 2024.08.15 |
CSE & GAME 개발 블로그
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 부탁드립니다!