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 캐릭을 만들어 보자
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);
}
SetViewTargetWithBlend()
함수는 카메라의 시점을 다른 Actor로 부드럽게 전환하는 데 사용되는 함수입니다. 이 함수는 주로 플레이어가 특정 액터나 카메라에서 시점을 바꾸거나 전환할 때 활용void SetViewTargetWithBlend( AActor* NewViewTarget, float BlendTime, EViewTargetBlendFunction BlendFunc = VTBlend_Linear, float BlendExp = 1.0f, bool bLockOutgoing = false );
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" });
}
}
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
}
Goal : Item, ItemData, ItemManager
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();
};
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);
}
}
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 반환
}