Unreal TIL 42일차 - 언리얼 컨테이너 형성

yys·2026년 4월 23일

TIL

목록 보기
34/85

오늘 한 내용


  • C++ 코드카타
  • Ch3 개인 프로젝트 구현
  • 게임 수학 강의 수강
  • 언리얼 마스터 반 과제 수행

UI 제작


오늘은 마스터 반 과제를 해결하기 위해 조금 시간을 썼다.
구현 내용은 다음과 같았다.

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("타겟 변경"));
	}
}

여기서 사용된 주요 함수는 다음과 같다.

  • UGameplayStatics::DeprojectScreenToWorld - 2D 화면상의 픽셀 좌표를 3D 월드의 좌표와 방향으로 변환
  • LineTraceSingleByChannel - 채널을 기반으로 라인 트레이스 수행

이후 아이템 액터를 블루프린트로 만들고 Item Collision Preset을 적용해준 뒤, 라인 트레이스 채널 세팅까지 해주었다.

결과는 다음과 같다.

컨테이너 구성 및 입력 처리


이제 라인 트레이스도 잘 구현되는 것을 확인했으니, 아이템을 획득하는 로직을 구현하려고 한다.

흐름은 다음과 같다.

  1. 아이템 클래스를 구현하고 아이템 이름, 요구 칭호, 칭호 아이템 여부, 획득 칭호를 나타내는 멤버 변수를 보유한다.
  2. 플레이어는 아이템을 담는 가방, 아이템 - 정보 매핑, 획득한 칭호를 각각 TArray/TMap/TSet으로 멤버 변수를 정의한다.
  3. E키로 플레이어의 Interact 함수를 열람한다.
    3-1. 만약 아이템이 라인 트레이스로 지정된 상태라면 아이템을 획득한다.
    3-2. 아니라면 현재 가방 상태를 출력한다.
  4. 3번에서 아이템이 요구하는 칭호를 보유하고 있지 않다면 리턴한다.

아이템 클래스

#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));
		}
	}
}

이를 바탕으로 아이템을 여러 개 정의 해주었고, 결과는 다음과 같다.

profile
게임 개발 지망생

0개의 댓글