[UE5] Lena: Dev Diary #16 - 아이템 상호작용 개선 및 코드 리팩토링

ChangJin·2024년 7월 26일
0

Unreal Engine5

목록 보기
88/114
post-thumbnail

2024.07.26

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

이번 포스팅에서는 플레이 중 발생한 문제 상황과 이를 해결하는 방법들을 다룹니다. 인벤토리 상호작용을 개선한 내용과 아이템 소리 문제, 문과 관련된 위젯 및 히트박스 처리, 그리고 키 아이템 처리 문제 등을 중점적으로 설명합니다.

진행상황

  • ✅ 아이템
    • ✅ 아이템 주울 때 소리가 나지 않는 문제 해결
    • ✅ 아이템 떨어뜨릴 때 소리가 나지 않는 문제 해결
    • ◻️ 총 아이템을 인벤토리에 넣기
    • ◻️ 총 아이템을 들고 있지 않으면 총알 관련 UI 뜨지 않게 하기
  • ✅ 문
    • ✅ 문을 열 때 위젯 삭제
    • ✅ ButtonLock, DirectionalLock, CombinationLock 관련 문제
  • ✅ 키 아이템 문제
상호작용 관련 Class 상속 관계도
자물쇠 관련 의존 관계도
아이템 관리 시스템 구성도
업데이트 된 상호작용 관련 상속 관계도

JSON 형식으로 데이터 테이블 관리하기

이렇게 struct를 만들어 두고 이걸 기반으로 데이터 테이블을 만들었습니다.

#pragma once

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

USTRUCT(BlueprintType)
struct FConditionEntry
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FString ConditionType; // "Lock", "Item", "Dialogue", "Location", etc

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TArray<FString> ConditionID; // LockID, ItemID, NPCID, LocionID, etc

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TArray<FString> AdditionalData; // PassWord, DialogueLine, Location, etc
};

USTRUCT(BlueprintType)
struct FActorEntry : public FTableRowBase
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FString ActorID;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TArray<FConditionEntry> RequiredConditions;
};

만든 데이터 테이블에 다음처럼 값을 추가해서 넣었습니다:

이렇게 하지 말고 JSON 형식으로 파일을 만들고, 거기에 값을 적어 넣은 후 리임포트로 가져온다면 더 관리가 쉬워집니다.

JSON 파일 작성

먼저 JSON 파일을 다음과 같이 작성합니다:

[
    {
        "Name": "Row_0",
        "ActorID": "BP_HingedDoor_Show",
        "RequiredConditions": [
            {
                "ConditionType": "Lock",
                "ConditionID": ["BP_ButtonLock"],
                "AdditionalData": ["1367"]
            }
        ]
    },
    {
        "Name": "Row_1",
        "ActorID": "BP_SlidingDoor",
        "RequiredConditions": [
            {
                "ConditionType": "Item",
                "ConditionID": ["Key"],
                "AdditionalData": [""]
            }
        ]
    },
    {
        "Name": "Row_2",
        "ActorID": "BP_SlidingDoor2",
        "RequiredConditions": [
            {
                "ConditionType": "Item",
                "ConditionID": ["Key"]
            }
        ]
    },
    {
        "Name": "Row_3",
        "ActorID": "BP_SlidingDoor3",
        "RequiredConditions": [
            {
                "ConditionType": "Lock",
                "ConditionID": ["BP_DirectionalLock"],
                "AdditionalData": ["21134"]
            }
        ]
    },
    {
        "Name": "Row_4",
        "ActorID": "BP_SlidingDoor4",
        "RequiredConditions": [
            {
                "ConditionType": "Default"
            }
        ]
    },
    {
        "Name": "Row_5",
        "ActorID": "BP_SlidingDoor5",
        "RequiredConditions": [
            {
                "ConditionType": "Default"
            }
        ]
    },
    {
        "Name": "Row_6",
        "ActorID": "BP_HingedDoor_Show2",
        "RequiredConditions": [
            {
                "ConditionType": "Lock",
                "ConditionID": ["BP_CombinationLock"],
                "AdditionalData": ["5237"]
            }
        ]
    },
    {
        "Name": "Row_7",
        "ActorID": "BP_SlidingDoor6",
        "RequiredConditions": [
            {
                "ConditionType": "Default"
            }
        ]
    },
    {
        "Name": "Row_8",
        "ActorID": "BP_SlidingDoor7",
        "RequiredConditions": [
            {
                "ConditionType": "Item",
                "ConditionID": ["Clock"],
                "AdditionalData": [""]
            }
        ]
    },
    {
        "Name": "Row_9",
        "ActorID": "BP_SlidingDoor8",
        "RequiredConditions": [
            {
                "ConditionType": "Button",
                "ConditionID": ["BP_Terminal"],
                "AdditionalData": ["Microwave"]
            }
        ]
    },
    {
        "Name": "Row_10",
        "ActorID": "BP_SlidingDoor9",
        "RequiredConditions": [
            {
                "ConditionType": "Button",
                "ConditionID": ["BP_Terminal2"],
                "AdditionalData": ["Microwave"]
            }
        ]
    }
]

JSON 파일을 데이터 테이블로 변환하기

언리얼 엔진에서 데이터 테이블을 생성하고 리임포트로 JSON 파일을 가져오면 됩니다.

JSON 파일 임포트 후에 필요한 C++ 코드 실행하여 데이터 테이블로 변환하는 작업을 수행하면 됩니다.

이 방법을 사용하면 JSON 파일을 기반으로 데이터 테이블을 생성하고, 이를 언리얼 엔진에서 사용할 수 있습니다. JSON 파일에서 각 행에 고유한 Name 필드를 추가함으로써 오류를 방지할 수 있습니다.

문제 상황과 해결 방법

1. 아이템 주울 때 소리가 나지 않음

아이템을 주울 때 소리가 나지 않는 문제가 발생했습니다. 이를 해결하기 위해 UGameplayStatics::PlaySoundAtLocation을 추가했습니다.

UPROPERTY(EditAnywhere)
USoundBase* PickupItemSound;

void ABase_Character::PickupItem(AActor* ItemActor)
{
    if (ItemActor)
    {
        ABase_Item* Item = Cast<ABase_Item>(ItemActor);
        if(Item)
        {
            FInventoryItem InventoryItem;
            InventoryItem.ItemID = Item->ItemID;
            InventoryItem.ItemName = Item->ItemName;
            InventoryItem.ItemImage = Item->ItemImage;
            InventoryItem.Quantity = Item->Quantity;
            InventoryItem.weight = Item->weight;
            InventoryItem.ItemDescription = Item->ItemDescription;
            InventoryItem.ItemActor = Item;
            
            if (InventoryComponent)
            {
                InventoryComponent->AddItem(InventoryItem);
                UGameplayStatics::PlaySoundAtLocation(GetWorld(), PickupItemSound, GetActorLocation());
            }
            
            ItemActor->Destroy();
        }
    }
}

* 각 문에서 위젯 관리

기존에는 각 문에서 다음처럼 위젯과 hitbox를 삭제하고 있었습니다.

if(WidgetComponent)
		{
			UUserWidget* Widget = WidgetComponent->GetWidget();
			UInteractWidget* InteractWidget = Cast<UInteractWidget>(Widget);
			InteractWidget->SetInstructionColor(FColor::Blue);
			InteractWidget->SetInstruction(FText::FromString(WidgetDisplayPassword));
			InteractWidget->InstructionText = FText::FromString("Congratulations! You solved the puzzle!");
			InteractWidget->SetInstructionColor(FColor::Green);
		}

그러나 모든 곳에서 이렇게 쓰다보니 코드가 길어지게 되었습니다. 현재 모든 상호작용 오브젝트의 Base Class로 있는 InteractActor에서 다음처럼 위젯과 HitBox를 파괴하는 함수를 만들어 두었습니다. 위젯과 HitBox는 Protected라서 다른 클래스에서는 접근이 불가능했기 때문이었습니다.


void AInteractableActor::DestroyHitBoxAndWidgetDelayFunction(float DelayTime)
{
	FTimerDelegate  Timer;
	Timer.BindUFunction(this, FName("DestroyInstructionWidget"));
	Timer.BindUFunction(this, FName("DestroyHitBox"));

	GetWorld()->GetTimerManager().SetTimer(WrongAnswerDelayHandle, Timer, DelayTime, false);
}


void AInteractableActor::DestroyInstructionWidget()
{
	WidgetComponent->DestroyComponent();
}

void AInteractableActor::DestroyHitBox()
{
	HitBox->DestroyComponent();
}

마찬가지로 Widget에 텍스트 내용과 텍스트 컬러를 바꾸는 부분도 BaseClass인 InteractActor에서 만들어두었습니다.

void AInteractableActor::ClearInstructionWidgetTextDelay(float DelayTime)
{
	FTimerDelegate  Timer;
	Timer.BindUFunction(this, FName("WrongAnswerDelayFunction"));

	GetWorld()->GetTimerManager().SetTimer(WrongAnswerDelayHandle, Timer, DelayTime, false);
}

void AInteractableActor::WrongAnswerDelayFunction()
{
	if(WidgetComponent)
	{
		UUserWidget* Widget = WidgetComponent->GetWidget();
		UInteractWidget* InteractWidget = Cast<UInteractWidget>(Widget);
		InteractWidget->SetInstruction(FText::FromString("Press E"));
		InteractWidget->SetColorAndOpacity(FLinearColor::White);
	}
}

void AInteractableActor::SetInstructionWidgetText(FText Text, FLinearColor Color)
{
	if(WidgetComponent)
	{
		UUserWidget* Widget = WidgetComponent->GetWidget();
		UInteractWidget* InteractWidget = Cast<UInteractWidget>(Widget);
		InteractWidget->SetInstruction(Text);
		InteractWidget->SetColorAndOpacity(Color);
	}
}

void AInteractableActor::SetInstructionWidgetTextAtBeginPlay(FText Text)
{
	if(WidgetComponent)
	{
		UUserWidget* Widget = WidgetComponent->GetWidget();
		UInteractWidget* InteractWidget = Cast<UInteractWidget>(Widget);
		InteractWidget->InstructionText = Text;
	}
}

2. BP_Terminal에서 문을 열 때 위젯 삭제

문을 열 때 관련된 위젯과 히트박스를 삭제해야 했습니다. 이를 위해 DestroyComponent 함수를 추가하여 문제를 해결했습니다.

초기 코드

void AButtonActor::OpenDoor()
{
    if(CheckConditions())
    {
        DoorActor->Open();
    }
    else
    {
        SetInstructionWidgetText(FText::FromString("Locked!"), FLinearColor::Red);
        ClearInstructionWidgetTextDelay(0.5);
        if(WrongAnswerCameraShakeClass)
        {
            GetWorld()->GetFirstPlayerController()->ClientStartCameraShake(WrongAnswerCameraShakeClass);
        }

        UGameplayStatics::PlaySoundAtLocation(GetWorld(), WrongAnswerSound, GetActorLocation(),1.0f, 2.0f);
    }
}

개선된 코드

void AButtonActor::OpenDoor()
{
    if(CheckConditions())
    {
        DoorActor->RequiredCondition = "Default";
        SetInstructionWidgetText(FText::FromString("Opend!"), FLinearColor::Green);
        UGameplayStatics::PlaySoundAtLocation(GetWorld(), RightAnswerSound, GetActorLocation());
        DoorActor->Open();
        DestroyHitBoxAndWidgetDelayFunction(1.0);
		DoorActor->DestroyHitBoxAndWidgetDelayFunction(1.0);
    }
    else
    {
        SetInstructionWidgetText(FText::FromString("Locked!"), FLinearColor::Red);
        ClearInstructionWidgetTextDelay(0.5);
        if(WrongAnswerCameraShakeClass)
        {
            GetWorld()->GetFirstPlayerController()->ClientStartCameraShake(WrongAnswerCameraShakeClass);
        }

        UGameplayStatics::PlaySoundAtLocation(GetWorld(), WrongAnswerSound, GetActorLocation(),1.0f, 2.0f);
    }
}

3. ButtonLock 열리면 문에 위젯 삭제

ButtonLock이 열리면 문에 위젯을 삭제해야 했습니다. 이를 위해 DestroyComponent를 사용하여 관련 컴포넌트를 제거했습니다.

초기 코드

void AButtonLockActor::DestroyButtonLock()
{
    ZoomOutCamera();
}

개선된 코드

void AButtonLockActor::DestroyButtonLock()
{
    ZoomOutCamera();
    DestroyHitBoxAndWidgetDelayFunction(1.0);
	DoorActor->DestroyHitBoxAndWidgetDelayFunction(1.0);
}

4. DirectionalLock 풀면 문에 위젯 삭제

DirectionalLock을 풀 때 문에 위젯을 삭제해야 했습니다. 이를 위해 DestroyComponent를 사용하여 관련 컴포넌트를 제거했습니다.

초기 코드

void ADirectionalLockActor::Unlock(AActor* ActorToUnlock)
{
    Super::Unlock(ActorToUnlock);
    ADoorActor* Door = Cast<ADoorActor>(ActorToUnlock);
    if(Door)
    {
        Door->Open();
    }
}

개선된 코드

void ADirectionalLockActor::Unlock(AActor* ActorToUnlock)
{
    Super::Unlock(ActorToUnlock);
    ADoorActor* Door = Cast<ADoorActor>(ActorToUnlock);
    if(Door)
    {
        Door->Open();
       DestroyHitBoxAndWidgetDelayFunction(1.0);
		DoorActor->DestroyHitBoxAndWidgetDelayFunction(1.0);
    }
}

5. DirectionalLock 정답이면 파란색으로 나오게끔 하기

DirectionalLock의 정답을 맞추면 파란색으로 나오도록 설정했습니다.

초기 코드

if(CheckPassword(InputPassWord))
{
    Unlock(TargetDoor);
    DirectionalLockBodyMeshComponent->SetSimulatePhysics(true);
    DirectionalLockshackleMeshComponent->SetSimulatePhysics(true);
    DirectionalLockBallMeshComponent->SetSimulatePhysics(true);

    if(WidgetComponent)
    {
        UUserWidget* Widget = WidgetComponent->GetWidget();
        UInteractWidget* InteractWidget = Cast<UInteractWidget>(Widget);
        InteractWidget->SetInstructionColor(FColor::Blue);
        InteractWidget->SetInstruction(FText::FromString(WidgetDisplayPassword));
        InteractWidget->InstructionText = FText::FromString("Congratulations! You solved the puzzle!");
        InteractWidget->SetInstructionColor(FColor::Green);
    }
}

개선된 코드

if(CheckPassword(InputPassWord))
{
    Unlock(TargetDoor);
    DirectionalLockBodyMeshComponent->SetSimulatePhysics(true);
    DirectionalLockshackleMeshComponent->SetSimulatePhysics(true);
    DirectionalLockBallMeshComponent->SetSimulatePhysics(true);

    SetInstructionWidgetText(FText::FromString(WidgetDisplayPassword), FColor::Blue);
    SetInstructionWidgetText(FText::FromString("Congratulations! You solved the puzzle!"), FLinearColor::Green);
}

6. 위젯 코드가 너무 길음

위젯 코드가 너무 길어 리팩터링이 필요했습니다. 이를 해결하기 위해 별도의 함수로 분리했습니다.

초기 코드

if(WidgetComponent)
{
    UUserWidget* Widget = WidgetComponent->GetWidget();
    UInteractWidget* InteractWidget = Cast<UInteractWidget>(Widget);
    InteractWidget->SetInstructionColor(FColor::Red);
    InteractWidget->SetInstruction(FText::FromString(WidgetDisplayPassword));
    InteractWidget->SetInstruction(FText::FromString("Try Again!"));
    InteractWidget->InstructionText = FText::FromString("Try Again!");
}

개선된 코드

SetInstructionWidgetText(FText::FromString("Try Again!"), FLinearColor::Red);

7. CombinationLock에서 정답을 맞춘 후에도 마우스 휠을 돌리면 버튼이 돌아가는 문제

CombinationLock에서 정답을 맞춘 후에도 마우스 휠을 돌리면 버튼이 돌아가는 문제를 해결했습니다.

초기 코드

void ACombinationLockActor::CheckRightAnswer()
{
    if(CheckPassword(GetCurrentDial()))
    {
        Unlock(TargetDoor);
        CheckAnim = true;
        for(UStaticMeshComponent* InMesh : WheelMeshArray)
        {
            InMesh->SetSimulatePhysics(true);
        }
    }
}

개선된 코드

void ACombinationLockActor::CheckRightAnswer()
{
    if(CheckPassword(GetCurrentDial()))
    {
        Unlock(TargetDoor);
        CheckAnim = true;
        for(UStaticMeshComponent* InMesh : WheelMeshArray)
        {
            InMesh->SetSimulatePhysics(true);
        }
        DestroyHitBoxAndWidgetDelayFunction(1.0);
		DoorActor->DestroyHitBoxAndWidgetDelayFunction(1.0);
    }
}

8. 분명 Key 아이템이 있는데 문이 열리지 않는 문제

키 아이템이 있음에도 문이 열리지 않는 문제를 해결했습니다. 가지고 있는 아이템에 문을 열기 위한 아이템이 있는지 없는지 판단하는 로직에서 실수가 있었습니다.

초기 코드

TMap<FString, AActor*> CheckItemMap;
for(const FInventoryItem& InventoryItem : Character->InventoryComponent->Items)
{
    CheckItemMap.Add(InventoryItem.ItemName, InventoryItem.ItemActor);
}

if(CheckItemMap.Num() == RequiredItem.Num())
{
    for(const TPair<FString, AActor*>& ItemPair : CheckItemMap)
    {
        Character->InventoryComponent->RemoveItemByName(ItemPair.Key);
    }
    return true;
}
return false;

개선된 코드

TMap<FString, bool> CheckItems;

for(const FString Item : RequiredItem)
{
    CheckItems.Add(Item, false);
}

for(const FInventoryItem& InventoryItem : Character->InventoryComponent->Items)
{
    FString ItemName = InventoryItem.ItemName;
    if(CheckItems.Find(ItemName))
    {
        CheckItems[ItemName] = true;
    }
}

for(const TPair<FString, bool> ItemPair : CheckItems)
{
    if(!ItemPair.Value)
    {
        return false;
    }
}

return true;

결론

이번 작업을 통해 다양한 문제를 해결하고 코드 리팩터링을 수행하였습니다. 아이템 소리 문제, 문과 관련된 위젯 및 히트박스 처리, 키 아이템 처리 문제 등을 중점적으로 다뤘습니다. 이를 통해 게임 내 상호작용 시스템이 더욱 견고하고 유연해졌습니다.

앞으로도 다양한 문제를 해결하고 코드를 개선하는 작업을 계속해나갈 예정입니다. 이이 포스팅을 통해 여러분도 다양한 문제를 해결하고 코드 리팩터링을 수행해보세요! 질문이나 피드백은 댓글로 남겨주세요. 도움이 되셨으면 좋겠네요. 감사합니다!


profile
게임 프로그래머

0개의 댓글