[ Unreal Engine 5 / #11 Shop & Item & Inventory ]

SeungWoo·2024년 9월 23일
0

[ Ureal Engine 5 / 수업 ]

목록 보기
12/31
post-thumbnail

Interface

  • 본격적으로 사용할 맵을 만들기 위해 Ctrl+N를 누르고 Level를 생성한다

  • UInterface 클래스를 상속 받은 C++ 클래스를 하나 만든다
    • 이름은 InteractableInterface로 제작

InteractableInterface.h

#pragma once

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

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

class MAGICIAN_PROJECT_API IInteractableInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:

	// 상호작용 위젯을 표시하는 함수 ( 상호작용이 가능할 때 호출됨 )
	virtual void DisPlayInteractionWidget() = 0;

	// 상호 작용이 종료되었을 때 위젯을 숨기는 함수
	virtual void HideInteractionWidget() = 0;

	// 상호 작용이 실행할 때 호출되는 함수
	virtual void Interaction() = 0;

};
  • 상호작용 할 NPC 캐릭을 만들어 보자

    • Actor를 상속받은 NPC C++ 클래스
    • NPC를 상속받은 NPCShop C++ 클래스를 또 만들고
    • NPC 클래스를 상속받은 블루프린트를 만들어 보자
  • NPC는 공통으로 상호작용하는 기본 틀

    • 그안에 Shop 기능이 있는 NPC를 만들꺼기 때문에 공통 NPC 캐릭터의 틀을 만들어보자

NPC.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "InteractableInterface.h"
#include "NPC.generated.h"


UCLASS()
class MAGICIAN_PROJECT_API ANPC : public AActor, public IInteractableInterface
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ANPC();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

protected:
	// 위젯 컴포넌트
	UPROPERTY(EditAnywhere, Category = "Interaction")
	class UWidgetComponent* InteractionWidgetComp;

	// 모든 NPC들이 공통으로 들고 다닐 부분을 정의
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
	class UBoxComponent* BoxComp; 
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
	class UCameraComponent* CameraComp; // 상호 작용하면 Foucus
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
	class USkeletalMeshComponent* Skeletal;

	UPROPERTY(EditAnywhere, Category = "Interaction")
	TSubclassOf<UUserWidget> InteractionWidgetClass;



public:
	virtual void DisPlayInteractionWidget() override;
	virtual void HideInteractionWidget() override;
	virtual void Interaction() override;

};

NPC.cpp

#include "NPC.h"
#include "Components\BoxComponent.h"
#include "Camera\CameraComponent.h"
#include "Components\SkeletalMeshComponent.h"
#include "Components\WidgetComponent.h"
#include "Kismet\GameplayStatics.h"

// Sets default values
ANPC::ANPC()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	BoxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("InteractionBox"));
	SetRootComponent(BoxComp);
	CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("FocusCamera"));
	CameraComp->SetupAttachment(RootComponent);
	Skeletal = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Skeletal"));
	Skeletal->SetupAttachment(RootComponent);

	// 위젯 생성 및 초기화
	InteractionWidgetComp = CreateDefaultSubobject<UWidgetComponent>(TEXT("InteractionWidgetComponent"));
	InteractionWidgetComp->SetupAttachment(Skeletal);

	// 머리 위에 배치 : 스켈레탈 메쉬의 특정 소켓 또는 본에 부착
	InteractionWidgetComp->SetRelativeLocation(FVector(0, 0, 220.0f));
	InteractionWidgetComp->SetRelativeRotation(FRotator(0, 90.0f, 0));
	InteractionWidgetComp->SetWidgetSpace(EWidgetSpace::World);
	InteractionWidgetComp->SetDrawSize(FVector2D(50.0f, 50.0f));

	// 위젯 클래스를 지정하고 위젯을 생성하며 컴포넌트에 적용
	if (InteractionWidgetClass)
	{
		InteractionWidgetComp->SetWidgetClass(InteractionWidgetClass);
	}
}

// Called when the game starts or when spawned
void ANPC::BeginPlay()
{
	Super::BeginPlay();
	
	// 위젯을 비활성화 상태로 시작
	InteractionWidgetComp->SetVisibility(false);

}

// Called every frame
void ANPC::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ANPC::DisPlayInteractionWidget()
{
	if (InteractionWidgetComp)
	{
		InteractionWidgetComp->SetVisibility(true); // 위젯 표시 
	}
}

void ANPC::HideInteractionWidget()
{
	if (InteractionWidgetComp)
	{
		InteractionWidgetComp->SetVisibility(false); // 위젯 숨김
	}
}

void ANPC::Interaction()
{
	UE_LOG(LogTemp, Log, TEXT("Interacted with object"));
	// 상호 작용 로직

	APlayerController* myPlayerController = UGameplayStatics::GetPlayerController(this, 0);

	myPlayerController->SetViewTargetWithBlend(this, 1.0f);
	myPlayerController->SetInputMode(FInputModeUIOnly());
	myPlayerController->SetShowMouseCursor(true);
}
  • NPC와 상호 작용이 됐을때 머리위에 UI가 켜지도록 하는 간단한 로직을 만들 예정이다
  • SetViewTargetWithBlend() 함수는 카메라의 시점을 다른 Actor로 부드럽게 전환하는 데 사용되는 함수입니다. 이 함수는 주로 플레이어가 특정 액터나 카메라에서 시점을 바꾸거나 전환할 때 활용
void SetViewTargetWithBlend( AActor* 	NewViewTarget, float BlendTime, EViewTargetBlendFunction BlendFunc = VTBlend_Linear, float BlendExp = 1.0f, bool bLockOutgoing = false );

  • 에러 발생시 모듈에 "UMG" 추가한다

Build.cs

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class magician_Project : ModuleRules
{
	public magician_Project(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG" });
	}
}

  • 빌드 후 NPCShop 블루프린트의 카메라 위치나 캐릭의 위치를 잡아준다
  • NPCShop 블루프린트의 카메라 위치를 잡아준뒤

  • 임의로 사용할 UIWidget 블루프린트도 하나 만들어 대충 만들고 NPCShop블루 프린트에 선언한 UI 변수에 넣어준다
  • 단순 이미지로 표시하였지만,해당 위치에 UX Image, 나이아가라, 3D Icon Mesh 등 디자인 방향성은 여러가지

Interaction

  • 기본 세팅은 끝이 났다
  • 이제 LineTrace 로 하여금 해당 NPC 와 상호작용을 작성해보자
    • 기존에 작성한 LineTrace를 수정 해보자
    • Input E 값에 따라 레이저를 쏘던걸 내가 정해진 시간에 계속 쏴서 해당 충돌을 감지해서 Actor 변수에 저장했다 E키를 눌렀을때 상호작용하게 할 생각이다
    • 기존에 있던 E입력 함수에 로직을 PerformanceInteractionTrace라는 함수를 만들어 이동 시킨다

CTPSPlayer.h

private:
	AActor* CachedInteractableActor; // 현재 상호작용 중인 엑터를 저장
	void PerformanceInteractionTrace();

CTPSPlayer.cpp

void ACTPSPlayer::BeginPlay()
{
	Super::BeginPlay();
	
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			subsystem->AddMappingContext(PlayerMappingContext,0);
		}
	}

	// 일정 주기마다 Trace로 실행하는 타이머 설정 ( 0.2f )
	FTimerHandle TraceTimerHanle; 
	GetWorldTimerManager().SetTimer(TraceTimerHanle, this, &ACTPSPlayer::PerformanceInteractionTrace, 0.2f, true);

}

void ACTPSPlayer::InputInteraction(const FInputActionValue& value)
{
	// 상호작용 수행임시

	if (CachedInteractableActor)
	{
		IInteractableInterface* InteractableActor = Cast< IInteractableInterface>(CachedInteractableActor);

		InteractableActor->Interaction();
	}
}



void ACTPSPlayer::PerformanceInteractionTrace()
{
#pragma region sigle Trace

	// 시작점
	FVector _Start = GetActorLocation();
	// 끝점
	FVector _End = _Start + GetActorForwardVector() * 2000.0f;
	// Trace 결과 값 struct = BreakResult
	FHitResult _HitOut;

	FCollisionQueryParams _TraceParams;
	_TraceParams.AddIgnoredActor(this); // 본인은 안맞도록

	bool bHit = GetWorld()->LineTraceSingleByChannel(_HitOut, _Start, _End, ECC_EngineTraceChannel2, _TraceParams);

	if (bHit)
	{
		//  맞은 엑터를 저장
		AActor* HitActor = _HitOut.GetActor();
		// 해당 엑터에 특정 인터페이스를 소유하고 있는지 여부를 알고 싶으면, Casting 해보면 된다
		IInteractableInterface* InteractableActor = Cast< IInteractableInterface>(HitActor);
		if (InteractableActor)
		{
			// 새로운 상호작용 대상이 이전 상호작용 대상과 다르면??
			if (CachedInteractableActor != HitActor)
			{
				// 이전 상호작용 대상이 있었다면 위젯을 숨김
				if (CachedInteractableActor)
				{
					IInteractableInterface* CachedInteractable = Cast< IInteractableInterface>(CachedInteractableActor);
					if (CachedInteractable)
					{
						CachedInteractable->HideInteractionWidget();
					}
				}

				// 새로운 엑터를 캐시하고 위젯 표시
				CachedInteractableActor = HitActor;
				InteractableActor->DisPlayInteractionWidget();
			}

			// 상호 작용 실행
			//InteractableActor->Interaction();
		}

		// 디버깅
		GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Green, FString::Printf(TEXT("Hit Actor : %s"), *_HitOut.GetActor()->GetName()));
		DrawDebugLine(GetWorld(), _Start, _HitOut.ImpactPoint, FColor::Red, false, 5.0f, 5.0f);
		DrawDebugSphere(GetWorld(), _HitOut.ImpactPoint, 10.0f, 12, FColor::Yellow, false, 5.0f);
	}
	else
	{

		// 디버깅
		GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, FString::Printf(TEXT("Hit failed")));
		DrawDebugLine(GetWorld(), _Start, _End, FColor::Blue, false, 5.0f, 5.0f);
	}

#pragma endregion

}
  • Tick 에서 매 프레임 호출하여도 무관하나,
    전투 및 액션과 같은 매프레임 연산할만큼의 민감성은 없기에 타이머로 주기적으로 확인하도록 설정

Item 기본 세팅

Goal : Item, ItemData, ItemManager

  • Item 구조체 생성
  • FItemData 구조체 :
    • 아이템의 기본 데이터를 저장하고, DataTable과 함께 아이템의 속성을 관리하는 구조체입니다.
    • 필드 : 아이템 ID, 이름, 설명, 아이콘, 메쉬, 가격, 타입, 스택 가능 여부 등 다양한 속성을 포함합니다.
    • 용도 : 각 아이템의 데이터베이스 역할을 하며, DataTable을 통해 아이템 데이터를 로드 및 참조할 수 있습니다.
  • UItemManager 클래스 :
    • 아이템 데이터를 관리하는 클래스입니다. 아이템 데이터를 로드하고, ItemID로 아이템을 검색할 수 있는 기능을 제공합니다.
    • 필드 : TMap<int32, FItemData>를 사용하여 ItemID를 키로 아이템 데이터를 저장합니다.
    • 기능 :
      • LoadItemData : DataTable에서 아이템 데이터를 로드하여 맵에 저장합니다.
      • GetItemDataByID : ItemID로 아이템 데이터를 검색하여 반환합니다.
      • GetItemDataByIDOptional : 안전한 검색을 위해 TOptional을 사용한 검색 기능을 제공합니다.
  • 구조체로 사용할 Non 타입의 구조체를 사용하기 위해 C++ 클래스를 하나 만든다
    • 이름은 ItemData로 만든다

ItemData.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "ItemData.generated.h"


UENUM(BlueprintType)
enum class EItemType : uint8
{
	Consumble,	// 소모성 아이템 
	Equipment,	// 장착 가능한 아이템
	Queet,		// 퀘스트 아이템 
	Material	// 재료 아이템
};


USTRUCT(BlueprintType)
struct FItemData : public FTableRowBase
{
	GENERATED_BODY()
	
	// 아이템 고유 ID ( DetaTable 참조 )
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	int32 ItemID;
	
	// The type of the item : Equipment, Consumable, Quest, Material
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	EItemType ItemType;
	
	// Name of the item
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	FText ItemName;
	
	// Description of the item
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	FText ItemDescription;
	 
	// 2D thumbnail for inventory or shop display
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	UTexture2D* ItemThumbnail;

	// mesh to represent the item in the world
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	UStaticMesh* ItemMesh;

	// Item's base price in the shop
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	int32 Price;

	// whether the item is stackable 
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	bool bIsStackable;

	// Max stack count ( if applicable ) 
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	int32 MaxStackCount;
};

class MAGICIAN_PROJECT_API ItemData
{
public:
	ItemData();
	~ItemData();
};
  • 3D Object 존재하도록 하는 아이템 클래스로 Actor 형태의 클래스 C++를 제작

Item.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemData.h"
#include "Item.generated.h"

UCLASS()
class MAGICIAN_PROJECT_API AItem : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AItem();

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	FItemData ItemDate;

	// 아이템 습득
	UFUNCTION(BlueprintCallable, Category = "Item")
	void OnPickUp(class ACTPSPlayer* player);

	// 아이템 버림 
	UFUNCTION(BlueprintCallable, Category = "Item")
	void OnDrop(class ACTPSPlayer* Player);

	// 아이템 메쉬
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	UStaticMeshComponent* ItemComp;
};

Itme.cpp

#include "Item.h"
#include "CTPSPlayer.h"
#include "ItemData.h"

// Sets default values
AItem::AItem()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	ItemComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ItemMesh"));
	RootComponent = ItemComp;
}

void AItem::OnPickUp(ACTPSPlayer* player)
{
	if (player)
	{
		// 인벤토리에 아이템 추가 로직
		//player->AddItemToInventory(ItemData);
		Destroy(); // 습득후 삭제
	}
}

void AItem::OnDrop(ACTPSPlayer* Player)
{
	if (Player)
	{
		// 인벤토리에서 제거 및 월드에 아이템 생성
		FVector DropLocation = Player->GetActorLocation() + Player->GetActorForwardVector() * 100.0f;
		AItem* DroppedItem = GetWorld()->SpawnActor<AItem>(GetClass(), DropLocation, FRotator::ZeroRotator);
		DroppedItem->ItemDate = ItemDate;
		//Player->RemoveFromInventory(ItemData.ItemID);
	}
}
  • 후에 DataTable이라는 블루프린트를 만들 예정인데 담겨진 아이템들을 검색후 찾아 관리하는 Manager도 만들어 세팅
  • 다음 시간에 만들어볼 예정

  • Object 형태에 C++ 클래스를 하나 만든다
    • ItemManager 라는 이름으로 짓는다

ItemManager.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ItemData.h"
#include "ItemManager.generated.h"

/**
 * 
 */
UCLASS()
class MAGICIAN_PROJECT_API UItemManager : public UObject
{
	GENERATED_BODY()
	

public:
	UItemManager();

	// 아이템 데이터를 저장하는 맵( ItemId -> FItemData )
	TMap<int32, FItemData> ItemDataMap;

	// Data Table에서 아이템 데이터를 로드
	UFUNCTION()
	void LoadItemData(UDataTable* ItemDataTable);

	// ItemID로 아이템 데이터 검색
	UFUNCTION(BlueprintCallable, Category = "Item")
	TOptional<FItemData> GetItemDataByIDOptional(int32 ItemID);

	// ItemID로 아이템 데이터 검
	UFUNCTION(BlueprintCallable, Category = "Item")
	FItemData& GetItemDataByID(int32 ItemID);
};

ItemManager.cpp

#include "ItemManager.h"
#include "Engine\DataTable.h"
#include "Misc\Optional.h"

UItemManager::UItemManager()
{
}

void UItemManager::LoadItemData(UDataTable* ItemDataTable)
{
	if (ItemDataTable)
	{
		FString ContextString; 
		TArray<FItemData*> AllItems;
		ItemDataTable->GetAllRows(ContextString, AllItems);

		for (FItemData* Item : AllItems)
		{
			// ItemID를 키로 해서 아이템 데이터를 맵에 저장
			ItemDataMap.Add(Item->ItemID, *Item);
			UE_LOG(LogTemp, Warning, TEXT("Item loaded : %s "), *Item->ItemName.ToString());
		}
	}

}

// TOptional을 활용한 안전한 반환
TOptional<FItemData> UItemManager::GetItemDataByIDOptional(int32 ItemID)
{
	if (ItemDataMap.Contains(ItemID))
	{
		return ItemDataMap[ItemID]; // 값이 있으면 반환 
	}
	return TOptional<FItemData>(); // 값이 없으면 TOptional 반환 
}

// FItemData를 참조로 반환하기
FItemData& UItemManager::GetItemDataByID(int32 ItemID)
{
	// 맵에 아이템 ID가 있는지 확인
	if (ItemDataMap.Contains(ItemID))
	{
		// 해당하는 아이템 데이터를 참조로 반환
		return ItemDataMap[ItemID];
	}
	return ItemDataMap[0]; // 찾지 못했을 경우 ErrorItem 반환
}
profile
This is my study archive

0개의 댓글