2024.07.26
깃허브! | 풀리퀘! |
---|---|
★ https://github.com/ChangJin-Lee/Project-Lena | ★ https://github.com/ChangJin-Lee/Project-Lena/pull/15 |
이번 포스팅에서는 플레이 중 발생한 문제 상황과 이를 해결하는 방법들을 다룹니다. 인벤토리 상호작용을 개선한 내용과 아이템 소리 문제, 문과 관련된 위젯 및 히트박스 처리, 그리고 키 아이템 처리 문제 등을 중점적으로 설명합니다.
상호작용 관련 Class 상속 관계도 |
자물쇠 관련 의존 관계도 |
아이템 관리 시스템 구성도 |
업데이트 된 상호작용 관련 상속 관계도 |
이렇게 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 파일을 다음과 같이 작성합니다:
[
{
"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 파일 임포트 후에 필요한 C++ 코드 실행하여 데이터 테이블로 변환하는 작업을 수행하면 됩니다.
이 방법을 사용하면 JSON 파일을 기반으로 데이터 테이블을 생성하고, 이를 언리얼 엔진에서 사용할 수 있습니다. JSON 파일에서 각 행에 고유한 Name 필드를 추가함으로써 오류를 방지할 수 있습니다.
아이템을 주울 때 소리가 나지 않는 문제가 발생했습니다. 이를 해결하기 위해 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;
}
}
문을 열 때 관련된 위젯과 히트박스를 삭제해야 했습니다. 이를 위해 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);
}
}
ButtonLock이 열리면 문에 위젯을 삭제해야 했습니다. 이를 위해 DestroyComponent
를 사용하여 관련 컴포넌트를 제거했습니다.
void AButtonLockActor::DestroyButtonLock()
{
ZoomOutCamera();
}
void AButtonLockActor::DestroyButtonLock()
{
ZoomOutCamera();
DestroyHitBoxAndWidgetDelayFunction(1.0);
DoorActor->DestroyHitBoxAndWidgetDelayFunction(1.0);
}
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);
}
}
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);
}
위젯 코드가 너무 길어 리팩터링이 필요했습니다. 이를 해결하기 위해 별도의 함수로 분리했습니다.
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);
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);
}
}
키 아이템이 있음에도 문이 열리지 않는 문제를 해결했습니다. 가지고 있는 아이템에 문을 열기 위한 아이템이 있는지 없는지 판단하는 로직에서 실수가 있었습니다.
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;
이번 작업을 통해 다양한 문제를 해결하고 코드 리팩터링을 수행하였습니다. 아이템 소리 문제, 문과 관련된 위젯 및 히트박스 처리, 키 아이템 처리 문제 등을 중점적으로 다뤘습니다. 이를 통해 게임 내 상호작용 시스템이 더욱 견고하고 유연해졌습니다.
앞으로도 다양한 문제를 해결하고 코드를 개선하는 작업을 계속해나갈 예정입니다. 이이 포스팅을 통해 여러분도 다양한 문제를 해결하고 코드 리팩터링을 수행해보세요! 질문이나 피드백은 댓글로 남겨주세요. 도움이 되셨으면 좋겠네요. 감사합니다!