- C++ 코드카타
- Ch3 개인 프로젝트 구현
- 게임 수학 강의 수강
- 언리얼 마스터 반 과제 수행
오늘은 마스터 반 과제를 해결하기 위해 조금 시간을 썼다.
구현 내용은 다음과 같았다.
1. 가방은 TArray를 통해서 만든다.
2. TMap은 TArray에 담긴 아이템들을 확인하게 되면 그 아이템 정보를 Key값으로 아이템 정보들이 뜬다.
3. TSet은 칭호 획득 컨테이너로 선언.
4. 요구되는 칭호가 있어야 아이템을 사용할 수 있도록 만든다.
간단하게 키만 눌러서 구현할 수 있는 문제였는데, 나는 Tracing을 통해서 아이템 및 칭호를 획득할 수 있도록 조금 더 확장해보려고 했다.
먼저 3인칭 템플릿을 기반으로 틀을 가져온 후, 간단하게 크로스헤어 위젯을 만들어보았다.

이후, PlayerCharacter에 해당 위젯이 배치되도록 코드를 수정했다.
// ContainerTestCharacter.h
protected:
virtual void BeginPlay() override;
private:
// HUD 위젯
UPROPERTY(VisibleAnywhere)
UUserWidget* HUDWidget;
// 크로스헤어 클래스
UPROPERTY(EditAnywhere)
TSubclassOf<UUserWidget> CrossHairClass;
// ContainerTestCharacter.cpp
void AContainerTestCharacter::BeginPlay()
{
Super::BeginPlay();
if (CrossHairClass)
{
HUDWidget = CreateWidget<UUserWidget>(GetWorld(), CrossHairClass);
HUDWidget->AddToViewport();
}
}
결과

3인칭 템플릿과 조금 다르게, 카메라의 위치를 이동해서 크로스헤어가 잘 보이도록 했다.
이제, 아이템만 식별하기 위해 Trace 채널 및 Collision 채널을 추가해보려고 한다.
먼저, ProjectSettings - Trace Chanels에서 새로운 채널인 ItemTrace를 생성하고

이어서 Preset에서 다음과 같이 Item 프리셋도 추가해주었다.

그리고 스크린 중앙에서부터 월드까지 라인 트레이스를 수행하기 위해 다음과 같이 코드를 작성해주었다.
// ContainerTestCharacter.h
public:
virtual void Tick(float DeltaTime) override;
void ShowDebug(const FString& Message, const FColor& Color, float Duration) const;
private:
void TraceForItem();
// 트레이스 길이
UPROPERTY(EditAnywhere)
float TraceLength;
// 'ItemTrace' Trace 채널만 트레이싱 할 수 있도록 필터링
UPROPERTY(EditDefaultsOnly, Category = "Inventory")
TEnumAsByte<ECollisionChannel> ItemTraceChannel;
// Trace 판별을 위한 임시 액터
TWeakObjectPtr<AActor> ThisActor;
TWeakObjectPtr<AActor> LastActor;
// ContainerTestCharacter.cpp
void AContainerTestCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
TraceForItem();
}
void AContainerTestCharacter::ShowDebug(const FString& Message, const FColor& Color = FColor::White, const float Duration = 5.0f) const
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, Duration, Color, Message);
}
}
void AContainerTestCharacter::TraceForItem()
{
if (!IsValid(GEngine) || !IsValid(GEngine->GameViewport)) return;
FVector2D ViewportSize;
// 뷰포트 크기를 구한다.
GEngine->GameViewport->GetViewportSize(ViewportSize);
// 절반을 잘라서 뷰포트의 중앙을 구한다.
const FVector2D ViewportCenter = ViewportSize / 2.f;
FVector TraceStart;
FVector Forward;
APlayerController* PlayerController = Cast<APlayerController>(GetController());
if (!IsValid(PlayerController)||!UGameplayStatics::DeprojectScreenToWorld(PlayerController, ViewportCenter, TraceStart, Forward)) return;
const FVector TraceEnd = TraceStart + Forward * TraceLength;
FHitResult HitResult;
// 라인 트레이스 수행
GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ItemTraceChannel);
LastActor = ThisActor;
ThisActor = HitResult.GetActor();
if (ThisActor == LastActor) return;
if (ThisActor.IsValid())
{
ShowDebug(FString::Printf(TEXT("타겟: %s"), *ThisActor->GetName()));
}
if (LastActor.IsValid())
{
ShowDebug(TEXT("타겟 변경"));
}
}
여기서 사용된 주요 함수는 다음과 같다.
이후 아이템 액터를 블루프린트로 만들고 Item Collision Preset을 적용해준 뒤, 라인 트레이스 채널 세팅까지 해주었다.
결과는 다음과 같다.
이제 라인 트레이스도 잘 구현되는 것을 확인했으니, 아이템을 획득하는 로직을 구현하려고 한다.
흐름은 다음과 같다.
아이템 클래스
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemActor.generated.h"
class UStaticMeshComponent;
// ItemActor 클래스로 생성된 액터는 수동으로 아이템의 속성들을 지정해주어야 함.
UCLASS()
class CONTAINERTEST_API AItemActor : public AActor
{
GENERATED_BODY()
public:
AItemActor();
FName GetItemName() const { return ItemName; }
FName GetRequiredTitle() const { return RequiredTitle; }
bool IsTitle() const { return bIsTitle; }
FName GetRewardTitle() const { return RewardTitle; }
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item")
UStaticMeshComponent* MeshComponent;
// 아이템 이름
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
FName ItemName = NAME_None;
// 요구 칭호
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
FName RequiredTitle = NAME_None;
// 칭호 아이템인가?
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
bool bIsTitle = false;
// 칭호 보상
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
FName RewardTitle = NAME_None;
};
Interact 함수
// ContainerTestCharacter.cpp
void AContainerTestCharacter::Interact()
{
if (!ThisActor.IsValid())
{
PrintInventoryState();
return;
}
AItemActor* ItemActor = Cast<AItemActor>(ThisActor.Get());
if (!IsValid(ItemActor))
{
ShowDebug(FString::Printf(TEXT("아이템이 존재하지 않음: %s"), *ThisActor->GetName()), FColor::Red);
return;
}
if (PickupItem(ItemActor))
{
ItemActor->Destroy();
}
}
bool AContainerTestCharacter::PickupItem(const AItemActor* ItemActor)
{
if (!IsValid(ItemActor))
{
return false;
}
const FName ItemName = ItemActor->GetItemName();
const FName RequiredTitle = ItemActor->GetRequiredTitle();
// 요구 칭호가 이미 획득 칭호란에 있는지?
if (!ItemActor->IsTitle() && RequiredTitle != NAME_None && !OwnedTitles.Contains(RequiredTitle))
{
ShowDebug(FString::Printf(TEXT("칭호 %s가 없어 획득할 수 없습니다!"), *RequiredTitle.ToString()), FColor::Yellow);
return false;
}
// 해당 아이템이 칭호인지?
if (ItemActor->IsTitle())
{
const FName RewardTitle = ItemActor->GetRewardTitle();
if (RewardTitle == NAME_None)
{
ShowDebug(FString::Printf(TEXT("칭호 없음: %s"), *ItemName.ToString()), FColor::Red);
return false;
}
OwnedTitles.Add(RewardTitle);
ShowDebug(FString::Printf(TEXT("칭호 획득: %s"), *RewardTitle.ToString()), FColor::Green);
PrintInventoryState();
return true;
}
Inventory.Add(ItemName);
const FString ItemInfo = FString::Printf(TEXT("이름=%s, 요구 칭호=%s"), *ItemName.ToString(), *RequiredTitle.ToString());
ItemInfoMap.Add(ItemName, ItemInfo);
ShowDebug(FString::Printf(TEXT("획득한 아이템: %s"), *ItemName.ToString()), FColor::Green);
PrintInventoryState();
return true;
}
void AContainerTestCharacter::PrintInventoryState() const
{
for (const FName& Item : Inventory)
{
if (const FString* FoundInfo = ItemInfoMap.Find(Item))
{
ShowDebug(FString::Printf(TEXT("%s | %s"), *Item.ToString(), **FoundInfo));
}
}
}
이를 바탕으로 아이템을 여러 개 정의 해주었고, 결과는 다음과 같다.