[UE5] Lena: Dev Diary #8 - 퍼즐 기믹 완성 및 추가된 기능들

ChangJin·2024년 6월 26일
0

Unreal Engine5

목록 보기
74/114
post-thumbnail

2024.06.27

깃허브!풀리퀘!
https://github.com/ChangJin-Lee/Project-Lena https://github.com/ChangJin-Lee/Project-Lena/pull/7

이번 포스팅에서는 퍼즐 기믹의 완성, InteractionActor, PickupItem, DoorActor 클래스를 중심으로 추가된 기능들을 소개합니다. 이를 통해 게임의 몰입감을 한층 더 높였습니다.


진행상황

  • ✅ 퍼즐 기믹 완성
    - ✅ InteractionActor 클래스 추가
    - ✅ PickupItem 클래스 추가 및 기능 구현
    - ✅ DoorActor 클래스 추가 및 기능 구현
    - ✅ BP_SlidingDoor 블루프린트 구현
    - ✅ BP_Key 블루프린트 구현
    - ✅ BP_Hint 블루프린트 구현
    - ✅ BP_Terminal 블루프린트 구현

퍼즐 기믹 구현

퍼즐 기믹 상속 관계

이번 작업을 진행하면서 만들었던 상속 관계도입니다. 이를 기반으로 만들었습니다.

새로 만든 클래스와 블루프린트

인벤토리 구현 고민

아이템을 집어야 하는 상황에서, 상호작용이 가능한 ActorComponent를 만들 것인가 아니면 InteractionActor를 상속받은 PickupItem을 만들 것인가에 대한 고민이 있었습니다. 저는 후자를 선택했습니다. 이유는 ActorComponent에서 owner의 Root에 특정 컴포넌트를 추가해야 했기 때문입니다.

ActorComponent vs. 상속받은 PickupItem

ActorComponent 접근법

ActorComponent 접근법은 기본적으로 재사용성과 모듈화를 높일 수 있지만, 컴포넌트의 복잡성을 증가시킬 수 있습니다. 다음은 ActorComponent를 사용하는 예제입니다:

void UInteractableComponent::Interact()
{
    if (AActor* Owner = GetOwner())
    {
        // 상호작용 로직 구현
    }
}

이 방법의 장점은 여러 액터에 동일한 컴포넌트를 추가하여 재사용할 수 있다는 점입니다. 그러나 단점은 각 액터의 Root에 접근하여 컴포넌트를 추가해야 하며, 이 과정이 복잡해질 수 있다는 점입니다.

InteractionActor를 상속받은 PickupItem

이 접근법은 상호작용 기능을 구현한 기본 클래스인 InteractionActor를 상속받아 PickupItem을 생성하는 방식입니다. 이 방법의 주요 이점은 구현의 간결성과 상호작용 로직의 일관성입니다.

void APickupItem::PickUp()
{
    ABase_Character* Character = Cast<ABase_Character>(UGameplayStatics::GetPlayerCharacter(this, 0));
    if (Character)
    {
        Character->AddItemToInventory(ItemDetails);
        SetActorHiddenInGame(true);
        SetActorEnableCollision(false);
        SetActorTickEnabled(false);
    }
}

이 접근법은 상호작용 로직이 액터 클래스 내에서 직접 관리되므로, 코드의 가독성이 높아지고 유지보수가 용이해집니다.

결론

ActorComponent를 사용하는 방법은 재사용성과 모듈화에서 장점이 있지만, 상호작용이 필요한 액터마다 별도의 설정이 필요하며, 이 과정이 복잡해질 수 있습니다. 반면에, InteractionActor를 상속받은 PickupItem을 사용하는 방법은 구현의 간결성과 유지보수의 용이성을 제공합니다. 따라서, 상호작용 로직이 명확히 정의된 특정 아이템을 구현하는 데 더 적합하다고 판단하였습니다.

InteractionActor 클래스 구현

InteractionActor는 상호작용 가능한 액터를 구현하기 위한 기초 클래스입니다.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "InteractionActor.generated.h"

UCLASS()
class LENA_API AInteractionActor : public AActor
{
	GENERATED_BODY()
	
public:	
	AInteractionActor();

protected:
	virtual void BeginPlay() override;
	
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UUserWidget> PasswordWidget;

public:	
	virtual void Tick(float DeltaTime) override;

	UFUNCTION()
	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);

	UFUNCTION(BlueprintCallable)
	void AddWidget();

	UFUNCTION(BlueprintCallable)
	void RemoveWidget();

	UFUNCTION(BlueprintImplementableEvent)
	void OutSideEvent();

	UFUNCTION(BlueprintCallable)
	void HideTextRenderComponent();

	UFUNCTION(BlueprintCallable)
	void ShowTextRenderComponent();

	UPROPERTY(BlueprintReadWrite)
	bool IsDone = false;
	
private:
	UPROPERTY(VisibleAnywhere)
	USceneComponent* Root;

	UPROPERTY(VisibleAnywhere)
	UBoxComponent* HitBox;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
	UTextRenderComponent* TextRenderComponent;

	UPROPERTY(BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	APlayerController* PlayerController;

	UUserWidget* Widget;
};

핵심 코드:

  • OnOverlapBeginOnOverlapEnd 함수는 HitBox 컴포넌트에서 다른 액터와 겹칠 때 호출됩니다.
  • AddWidgetRemoveWidget 함수는 위젯을 화면에 추가하거나 제거하는 역할을 합니다.
void AInteractionActor::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	EnableInput(PlayerController);
	if (!IsDone)
	{
		ShowTextRenderComponent();
	}
}

void AInteractionActor::OnOverlapEnd(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	DisableInput(PlayerController);
	HideTextRenderComponent();
	OutSideEvent();
}

PickupItem 클래스 구현

PickupItem 클래스는 InteractionActor를 상속받아 상호작용 가능한 아이템을 구현합니다.

#pragma once

#include "CoreMinimal.h"
#include "Base_Item.h"
#include "InteractionActor.h"
#include "GameFramework/Actor.h"
#include "PickupItem.generated.h"

UCLASS()
class LENA_API APickupItem : public AInteractionActor
{
	GENERATED_BODY()
	
public:	
	APickupItem();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Item")
	ABase_Item* ItemDetails;

	UFUNCTION(BlueprintCallable)
	void PickUp();

	UFUNCTION(BlueprintCallable)
	void SetThisItemName(FName NewName);

private:
	UPROPERTY(EditAnywhere)
	UStaticMeshComponent* MeshComponent;
	
	UPROPERTY(EditAnywhere)
	USkeletalMeshComponent* SkeletalMeshComponent;
};

핵심 코드:

  • ItemDetails는 아이템의 상세 정보를 담고 있는 ABase_Item의 인스턴스입니다.
  • PickUp 함수는 아이템을 플레이어의 인벤토리에 추가하고, 아이템을 비활성화합니다.
void APickupItem::PickUp()
{
	ABase_Character* Character = Cast<ABase_Character>(UGameplayStatics::GetPlayerCharacter(this, 0));
	if (Character)
	{
		Character->AddItemToInventory(ItemDetails);
		SetActorHiddenInGame(true);
		SetActorEnableCollision(false);
		SetActorTickEnabled(false);
	}
}

DoorActor 클래스 구현

DoorActor 클래스는 InteractionActor를 상속받아 문을 여는 기능을 구현합니다.

#pragma once

#include "CoreMinimal.h"
#include "InteractionActor.h"
#include "DoorActor.generated.h"

UCLASS()
class LENA_API ADoorActor : public AInteractionActor
{
	GENERATED_BODY()

public:
	ADoorActor();

	UFUNCTION(BlueprintCallable)
	void OpenSlidingDoor(FVector Location);

	UFUNCTION(BlueprintImplementableEvent, Category="Inventory")
	void OpenDoorEvent();

	UFUNCTION(BlueprintCallable)
	bool CheckRequiredItem();

protected:
	virtual void BeginPlay() override;

private:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Door", meta = (AllowPrivateAccess = "true"))
	UStaticMeshComponent* MeshComponent;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Door", meta = (AllowPrivateAccess = "true"))
	FName RequiredItem;

	UPROPERTY(EditAnywhere, Category="Door")
	TSubclassOf<UCameraShakeBase> WrongAnswerCameraShakeClass;
};

핵심 코드:

  • OpenSlidingDoor 함수는 문을 여는 로직을 구현합니다.
  • CheckRequiredItem 함수는 플레이어가 문을 열기 위해 필요한 아이템을 소지하고 있는지 확인합니다.
void ADoorActor::OpenSlidingDoor(FVector Location)
{
	MeshComponent->SetRelativeLocation(Location);
}

bool ADoorActor::CheckRequiredItem()
{
	ABase_Character* Character = Cast<ABase_Character>(UGameplayStatics::GetPlayerCharacter(this, 0));
	if (Character && Character->HasItemInInventory(RequiredItem))
	{
		return true;
	}

	if (WrongAnswerCameraShakeClass)
	{
		GetWorld()->GetFirstPlayerController()->ClientStartCameraShake(WrongAnswerCameraShakeClass);
	}

	return false;
}

블루프린트 구현

  • BP_SlidingDoor: DoorActor를 기반으로 문이 슬라이딩으로 열리는 블루프린트입니다. 상호작용 시 WBP_Numpad 위젯이 나타나고, 올바른 비밀번호를 입력하면 문이 열립니다.

  • 여기서 만든 WBP_Numpad는 Canvas 위에 균등 그리드 패널을 사용해서 만들었습니다.

WBP_Numpad
  • 위젯에서의 로직은 다음과 같습니다.
WBP_Numpad
  • 굉장히 복잡해 보이지만 어렵지 않습니다. 하나씩 나눠보면 다음처럼 버튼 클릭에 대한 함수를 정의해줍니다.
버튼 클릭에 대한 함수
  • Enter, Clear에 대한 기능도 만들어줍니다.
Enter, Clear 함수
  • 문이 열리는 부분은 TimeLine을 써서 다음처럼 구현했습니다.
문 열리는 부분
  • BP_Key: PickupItem을 기반으로 만든 블루프린트로, 상호작용 시 아이템을 플레이어의 인벤토리에 추가합니다.
아이템 이름을 지정하고 PickUp

현재 아이템 이름을 각 블루프린트에서 직접 지정해야 하는 문제가 있습니다. 이를 해결하기 위해 고유한 값으로 바꿀 예정입니다.

  • BP_Hint: 힌트를 주는 위젯인 WBP_Hint를 상호작용 시 나타내는 블루프린트입니다.
위젯을 띄우는 로직힌트 위젯
  • BP_Terminal: 문을 여는 상호작용만 담당하는 블루프린트입니다.
Door의 문이 열리게끔 만듭니다

참고한 자료

profile
게임 프로그래머

0개의 댓글