[UE5] Lena: Dev Diary #11 - 방향 자물쇠 로직 구현

ChangJin·2024년 7월 5일
0

Unreal Engine5

목록 보기
77/114
post-thumbnail

2024.07.05

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

이번 포스팅에서는 방향 자물쇠 로직 구현 과정, 타임라인 사용법, Blender를 사용한 3D 에셋 편집, 문제 상황 해결 방법 등을 다룹니다. 특히, 방향 입력에 따른 자물쇠의 이동 처리 및 문과의 상호작용을 자세히 설명합니다.


진행상황

  • ✅ 조합 자물쇠
    • ✅ 각 칸 회전 정의
    • ✅ 문과의 상호작용 구현
    • ✅ 타임라인을 사용한 회전 처리
  • ✅ 방향 자물쇠
    • ✅ 방향 입력 정의
    • ✅ 문과의 상호작용 구현
    • ✅ 타임라인을 사용한 이동 처리
  • ⬜ 버튼 자물쇠

업데이트 된 상호작용 관련 상속 관계도

만들고자 하는 자물쇠

방향 자물쇠 로직 구현

이번 작업에서는 방향 자물쇠의 로직을 구현했습니다. 방향 자물쇠는 화살표 방향 키 입력에 따라 자물쇠가 이동하며, 올바른 방향 입력을 통해 문을 열 수 있도록 설계했습니다.

1. 3D 에셋 편집

방향 자물쇠의 3D 에셋이 없어서 기존의 일반 자물쇠 3D 에셋을 Blender로 편집하여 사용했습니다. Body, shackle, ball 이렇게 세 부분으로 나누어 직접 만들었고, fbx 파일로 내보내기 후 언리얼 엔진에서 컴포넌트로 조립했습니다.

Blender에서 본 기존 3D 에셋
기존 에셋에 shackle을 분리기존 에셋에 Body를 분리Blender에서 Ball 새로 만들기
Unreal Engine에서 Component로 구성

2. 코드 최적화

방향 입력에 따른 자물쇠의 이동 로직을 간결하게 만들기 위해 코드를 최적화했습니다.

a. 기존 코드

처음에는 switch 문을 사용하여 방향 입력을 처리했습니다.

switch (direction)
{
    case 1:
        WidgetDisplayPassword += "→";
        break;
    case 2:
        WidgetDisplayPassword += "←";
        break;
    case 3:
        WidgetDisplayPassword += "↑";
        break;
    case 4:
        WidgetDisplayPassword += "↓";
        break;
}

b. 최적화된 코드

조금 더 생각해보니 Enum Class를 사용하면 간결하게 줄일 수 있을 것이라고 생각했습니다. 그리고 두 줄로 간결하게 줄였습니다.

DirectionEnum DirectionEnumClass = static_cast<DirectionEnum>(FCString::Atoi(*direction));
WidgetDisplayPassword += EnumToString(DirectionEnumClass);

c. 한 줄로 줄일 수 있는 코드

조금 더더더 생각해보니 한 줄로도 줄일 수 있었습니다. FindObject를 사용하는 방법입니다.
그러나 이 방법은 코드 수는 획기적으로 줄일 수 있지만 그리 좋지 않은 방법입니다.

WidgetDisplayPassword += FindObject<UEnum>(ANY_PACKAGE, TEXT("DirectionEnum"), true)->GetDisplayNameTextByValue(FCString::Atoi(*direction)).ToString();

FindObject 사용의 문제점

FindObject를 사용하면 런타임 성능에 영향을 줄 수 있습니다. 반복적으로 호출될 경우 성능 저하가 발생할 수 있습니다. 공식 문서를 살펴보면 필요한 경우 초기화 시에 한 번만 사용하도록 권장하고 있습니다.
이번 작업에서 사용하지 않은 이유는 2가지 입니다.

이유:

  1. 비효율성: FindObject는 객체를 찾기 위해 많은 리소스를 소비할 수 있습니다. 반복적인 호출은 게임의 성능을 저하시킬 수 있습니다. 매번 검색하면 매우 좋지 않습니다. 가능한 한 객체 검색을 초기화 시 한 번만 하고, 이후에는 캐싱된 값을 사용해야 합니다.
  2. 런타임 안전성: 런타임에 객체를 찾는 과정에서 예기치 않은 에러가 발생할 수 있습니다. 이는 게임의 안정성을 저하시킬 수 있습니다.

3. 타임라인 설정 및 콜백 함수 정의

타임라인을 사용하여 자물쇠가 부드럽게 이동하도록 구현했습니다. 타임라인의 진행 상태에 따라 자물쇠의 위치를 업데이트하는 콜백 함수를 정의했습니다. Hinged Door와 Slide Door를 구현하면서 타임라인을 처음 접해봤는데 자물쇠를 구현하면서 하다보니 많이 익숙해졌습니다.

UTimelineComponent* DirectionalLockTimeLine;

	UPROPERTY()
	FOnTimelineFloat DirectionalLockTimeLineCallback;

	UPROPERTY()
	FOnTimelineEvent DirectionalLockTimeLineFinishedCallback;

	UFUNCTION()
	void HandleDirectionalLockProgress(float value);

	UFUNCTION()
	void HandleDirectionalLockFinished();

	UPROPERTY(EditAnywhere, Category="TimeLine")
	UCurveFloat* DirectionalLockCurve;

	bool bIsTimeLinePlaying = false;
void ADirectionalLockActor::BeginPlay()
{
    Super::BeginPlay();

    if(WidgetComponent)
    {
        UUserWidget* Widget = WidgetComponent->GetWidget();
        UInteractWidget* InteractWidget = Cast<UInteractWidget>(Widget);
        InteractWidget->SetInstructionAtBeginPlay(FText::FromString("Press Arrow Keys"));
    }

    if(DirectionalLockCurve)
    {
        DirectionalLockTimeLineCallback.BindUFunction(this, FName("HandleDirectionalLockProgress"));
        DirectionalLockTimeLine->AddInterpFloat(DirectionalLockCurve, DirectionalLockTimeLineCallback);
        DirectionalLockTimeLineFinishedCallback.BindUFunction(this,FName("HandleDirectionalLockFinished"));
        DirectionalLockTimeLine->SetTimelineFinishedFunc(DirectionalLockTimeLineFinishedCallback);
    }
}

4. 볼 이동 로직

볼이 중심에서 움직였다가 다시 중심으로 돌아오는 로직을 구현했습니다. 처음에는 바깥으로 움직이는 함수와 돌아오는 함수, 두 개를 사용하려 했으나, 하나의 커브로 해결할 수 있었습니다. 커브에서 0.0 -> 0.5 -> 1.0으로 설정하여 볼이 이동했다가 원래 자리로 돌아오게 만들었습니다.

볼의 이동 커브 설정
void ADirectionalLockActor::HandleDirectionalLockProgress(float value)
{
    if(DirectionalLockBallMeshComponent)
    {
        FVector NewLocation = FMath::Lerp(InitialLocation, TargetLocation, value);
        DirectionalLockBallMeshComponent->SetRelativeLocation(NewLocation);
    }
}

5. 방향 입력 처리

방향 입력에 따라 자물쇠를 이동시키는 로직을 구현했습니다. 각 방향 입력에 대해 EnumToString 함수를 사용하여 문자열로 변환하고 WidgetDisplayPassword에 추가했습니다.

방향 입력 처리
void ADirectionalLockActor::MoveFromStart(FVector InputVector, FString direction)
{
    if(bIsTimeLinePlaying) return;

    InitialLocation = DirectionalLockBallMeshComponent->GetRelativeLocation();
    TargetLocation = InitialLocation + InputVector;

    if(DirectionalLockTimeLine)
    {
        bIsTimeLinePlaying = true;
        DirectionalLockTimeLine->PlayFromStart();
    }

    InputPassWord += direction;

    DirectionEnum DirectionEnumClass = static_cast<DirectionEnum>(FCString::Atoi(*direction));
    WidgetDisplayPassword += EnumToString(DirectionEnumClass);

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

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

6. 문과의 상호작용

올바른 방향 입력이 완료되면 문이 열리도록 구현했습니다. 올바른 입력을 확인하고 문을 여는 로직을 추가했습니다.

다음에 리팩터링을 해야합니다. 조금만 대충 훑어봐도 중복되는 부분이 있습니다.

문과의 상호작용
void ADirectionalLockActor::CheckRightAnswer()
{
    if(GetNum(InputPassWord) < 5) return;

    TArray<AActor*> FindActors;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), DoorActorClass, FindActors);

    if

(FindActors.Num() >= 1)
    {
        for(int i = 0 ; i < FindActors.Num(); ++i)
        {
            DoorActor = Cast<ASlidingDoorActor>(FindActors[i]);

            if(DoorActor)
            {
                if(InputPassWord == DoorActor->GetPassWord())
                {
                    UGameplayStatics::PlaySoundAtLocation(GetWorld(), RightAnswerSound, GetActorLocation());
                    DoorActor->RightAnswer(FVector(0,130.0f,0));

                    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);
                    }
                }
                else
                {
                    UGameplayStatics::PlaySoundAtLocation(GetWorld(), WrongAnswerSound, GetActorLocation());

                    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!");
                    }

                    if(WrongAnswerCameraShakeClass)
                    {
                        GetWorld()->GetFirstPlayerController()->ClientStartCameraShake(WrongAnswerCameraShakeClass);
                    }
                    InputPassWord = "";
                    WidgetDisplayPassword = "";
                }
            }
        }
    }
}

7. Enum을 문자열로 변환

DirectionEnum을 문자열로 변환하여 사용할 수 있도록 EnumToString 함수를 정의했습니다.
보면 None이라는 entry가 있는 것을 볼 수 있는데요.

Enum을 문자열로 변환

Unreal Engine의 설계 원칙상 enum class의 entry는 0이 있어야 합니다. 즉 위 사진에서 None = 1, 이렇게 적을 수가 없습니다. 이렇게 적으면 오류납니다.

Warning: 'DirectionEnum' does not have a 0 entry! (This is a problem when the enum is initialized by default)

0 entry가 있어야 한다는 문구
FString ADirectionalLockActor::EnumToString(DirectionEnum EnumValue)
{
    const UEnum* EnumPtr = FindObject<UEnum>(ANY_PACKAGE, TEXT("DirectionEnum"), true);
    if (!EnumPtr)
    {
        return FString("Invalid");
    }

    return EnumPtr->GetDisplayNameTextByIndex(static_cast<int32>(EnumValue)).ToString();
}

문제 상황 및 해결 방법

이번 작업에서는 여러 가지 문제 상황이 발생했으며, 이를 해결하는 과정에서 많은 것을 배웠습니다.

문제 1: 콜백 함수 미등록

BeginPlay에서 콜백 함수를 등록할 때, HandleDirectionalLockFinished 함수를 정의하지 않아 문제가 발생했습니다.

콜백함수 등록을 실수하면 나는 오류
void ADirectionalLockActor::BeginPlay()
{
    Super::BeginPlay();

    if(WidgetComponent)
    {
        UUserWidget* Widget = WidgetComponent->GetWidget();
        UInteractWidget* InteractWidget = Cast<UInteractWidget>(Widget);
        InteractWidget->SetInstructionAtBeginPlay(FText::FromString("Press Arrow Keys"));
    }

    if(DirectionalLockCurve)
    {
        DirectionalLockTimeLineCallback.BindUFunction(this, FName("HandleDirectionalLockProgress"));
        DirectionalLockTimeLine->AddInterpFloat(DirectionalLockCurve, DirectionalLockTimeLineCallback);
        DirectionalLockTimeLineFinishedCallback.BindUFunction(this,FName("HandleDirectionalLockFinished"));
        DirectionalLockTimeLine->SetTimelineFinishedFunc(DirectionalLockTimeLineFinishedCallback);
    }
}

문제 2: 잘못된 함수 시그니처

HandleDirectionalLockFinished 함수를 매개변수를 가진 함수로 정의해서 문제가 발생했습니다. Finished로 정의할 함수는 매개변수가 없어야 합니다.

콜백으로 등록할 함수를 정의하지 않으면 나는 오류
void ADirectionalLockActor::HandleDirectionalLockFinished()
{
    bIsTimeLinePlaying = false;
}

문제 3: 타임라인 컴포넌트 생성 누락

생성자에서 타임라인 컴포넌트를 생성하지 않아 문제가 발생했습니다.

타임 라인 컴포넌트를 생성자에서 적어주는 것을 깜빡했을 때 나는 오류
ADirectionalLockActor::ADirectionalLockActor()
{
    PrimaryActorTick.bCanEverTick = true;
    
    ...

	// 바로 이 부분을 깜빡했음
    DirectionalLockTimeLine = CreateDefaultSubobject<UTimelineComponent>("DirectionalLockTimeLine");
    bIsTimeLinePlaying = false;
}

문제 4: 잘못된 플래그 체크

HandleDirectionalLockProgress 함수에서 bIsTimeLinePlaying 플래그를 잘못 사용하고 있었습니다. 이를 제거하여 문제를 해결했습니다. Progress 함수는 언제든지 실행할 수 있어야 합니다.

bIsTimeLinePlaying 플래그를 잘못 사용했을 때 나는 오류
void ADirectionalLockActor::HandleDirectionalLockProgress(float value)
{
    if(DirectionalLockBallMeshComponent)
    {
        FVector NewLocation = FMath::Lerp(InitialLocation, TargetLocation, value);
        DirectionalLockBallMeshComponent->SetRelativeLocation(NewLocation);
    }
}

문제 5: 여러 타임라인 충돌

다른 타임라인이 실행 중일 때 다른 화살표를 누르면 충돌이 발생하는 문제를 해결했습니다.

void ADirectionalLockActor::MoveFromStart(FVector InputVector, FString direction)
{
    if(bIsTimeLinePlaying) return;

	...
}

이번 작업을 통해 방향 자물쇠 로직을 완성하고, 문과의 상호작용을 구현하며, 코드의 유지보수성을 높일 수 있었습니다. 타임라인의 사용법이 더욱 익숙해졌고, Blender 지식을 통해 3D 에셋을 새로 만드는 데 많은 도움이 되었습니다. 작업하면서 에러가 많이 발생했지만 이전 포스팅 글과 공식 사이트를 참고해서 잘 해결할 수 있었습니다..

다음 작업은 방향 자물쇠 코드 리팩터링, 버튼 자물쇠 구현 입니다.

참고 자료


profile
게임 프로그래머

0개의 댓글