[UE5] Lena: Dev Diary #10 - 조합 자물쇠 로직 구현

ChangJin·2024년 7월 2일
0

Unreal Engine5

목록 보기
76/114
post-thumbnail

2024.07.02

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

이번 포스팅에서는 조합 자물쇠 로직 구현 과정, 회전 정의 방법, 문과의 상호작용, 문제 상황 해결 방법 등을 다룹니다. 특히, 각 칸의 회전 처리 및 타임라인 사용법에 대해 자세히 설명합니다.


진행상황

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

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

조합 자물쇠 로직 구현

이번 작업에서는 조합 자물쇠의 로직을 구현했습니다. 조합 자물쇠는 각각의 숫자 칸이 회전할 수 있으며, 올바른 비밀번호를 입력하면 문이 열리도록 설계했습니다. 간단하게 애니메이션 블루프린트를 만들어 자물쇠가 열리는 애니메이션도 추가하였습니다.

만들고자 하는 자물쇠

각 칸 회전 정의

각 칸의 회전 각도를 정의하는 데 여러 시도를 했습니다. 처음에는 switch case 문으로, 그 다음엔 if-else 문으로, 최종적으로는 36도로 나눈 몫을 사용하여 각도를 정의했습니다. 이렇게 하여 코드가 깔끔해졌습니다.

코드 예시

  1. 각의 범위에 따른 값 반환

이렇게 하면 논리상 문제가 없지만 Unreal Engine의 Roll은 0~360까지의 범위가 아니라 -180~180까지의 범위를 가지기 때문에 3에서 4로 넘어갈 때 문제가 생깁니다.

// 각도 범위에 따라 문자열을 반환
if (RollValue >= 36 && RollValue < 72)
{
    return FString("0");
}
else if (RollValue >= 72 && RollValue < 108)
{
    return FString("1");
}
else if (RollValue >= 108 && RollValue < 144)
{
    return FString("2");
}
else if (RollValue >= 144 && RollValue < 180)
{
    return FString("3");
}
else if (RollValue >= 180 && RollValue < 216)
{
    return FString("4");
}
else if (RollValue >= 216 && RollValue < 252)
{
    return FString("5");
}
else if (RollValue >= 252 && RollValue < 288)
{
    return FString("6");
}
else if (RollValue >= 288 && RollValue < 324)
{
    return FString("7");
}
else if (RollValue >= 324 && RollValue < 360)
{
    return FString("8");
}
else if (RollValue >= 0 && RollValue < 36)
{
    return FString("9");
}
return "Invalid";
  1. EnumClass를 선언하고 만들어주기
Enum Class 정의EnumClass 반환
다음 EnumClass로 넘어가기
  1. 값을 보정한 후에 몫으로 반환

그나마 제일 나은 방법이지만 더 좋은 방법이 떠오른다면 나중에 개선하려고 합니다.

float GetDialPosition(UStaticMeshComponent* WheelMeshDial)
{
    float RollValue = WheelMeshDial->GetRelativeRotation().Roll;
    RollValue = FMath::Fmod(RollValue + 180.0f, 360.0f) - 180.0f;

    if (RollValue < 0)
    {
        RollValue += 360.0f;
    }

    int PositionIndex = FMath::RoundToInt(RollValue / 36.0f) % 10;
    return static_cast<float>(PositionIndex);
}

문과의 상호작용

문과의 상호작용도 구현했습니다. 자물쇠의 비밀번호가 올바르면 문이 열리도록 했습니다. TSubclassOf로 에디터에서 받은 변수에 UGameplayStatics::GetAllActorsOfClass로 월드에 있는 원하는 액터를 가져왔고, FindActors.Num()으로 개수를 체크했습니다.

코드 예시

void ACombinationLockActor::CheckRightAnswer()
{
    TArray<AActor*> FindActors;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), DoorActorClass, FindActors);

    GetCurrentDial();
    if (FindActors.Num() >= 1)
    {
        DoorActor = Cast<ASlidingDoorActor>(FindActors[0]);

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

                CheckAnim = true;

                for (UStaticMeshComponent* InMesh : WheelMeshArray)
                {
                    InMesh->SetSimulatePhysics(true);
                }
            }
        }
    }
}

문제 상황 및 해결 방법

회전이 끝나지 않았는데 한 번 더 돌리려고 하면 각도가 조금씩 더해지거나 빼지는 문제가 있었습니다. 이를 해결하기 위해 TimelineFinishedCallback을 만들어 타임라인 시작 후 끝날 때까지는 입력이 안되도록 만들었습니다.

코드 예시

void ACombinationLockActor::ScrollCombinationLock(FRotator InTargetRotation)
{
    if (bIsTimeLinePlaying || !WheelMeshArray[SelectedWheelIndex]) return;

    InitialRotation = WheelMeshArray[SelectedWheelIndex]->GetRelativeRotation();
    TargetRotation = InitialRotation + InTargetRotation;
    if (CombinationLockTimeline)
    {
        bIsTimeLinePlaying = true;
        CombinationLockTimeline->PlayFromStart();
    }
}

void ACombinationLockActor::HandleCombinationLockProgress(float Value)
{
    if (WheelMeshArray[SelectedWheelIndex])
    {
        FRotator NewRotation = FMath::Lerp(InitialRotation, TargetRotation, Value);
        WheelMeshArray[SelectedWheelIndex]->SetRelativeRotation(NewRotation);
    }
}

void ACombinationLockActor::OnTimeLineFinished()
{
    bIsTimeLinePlaying = false;
}

자물쇠 회전 각도에 따른 숫자 표시 문제

문제상황

  • 자물쇠 회전 각도에 따른 숫자 계산에서 부정확한 값 반환하고 있었습니다. 자물쇠가 -36도 또는 -72도 등 음수 각도로 회전할 때 올바른 숫자가 표시되지 않았습니다. 원인은 자물쇠 회전 각도 처리 중에 회전이 연속해서 들어오면 음수 각도 처리에 오류가 생기기 때문이었습니다.

해결법

  • 자물쇠 회전 각도에 따른 숫자 계산 로직을 통해 사용자 인터페이스를 명확히 했습니다.
float GetDialPosition(UStaticMeshComponent* WheelMeshDial)
{
    // 현재 Roll 값을 가져옴
    float RollValue = WheelMeshDial->GetRelativeRotation().Roll;
    // Roll 값을 -180에서 180 범위로 정규화
    RollValue = FMath::Fmod(RollValue + 180.0f, 360.0f) - 180.0f;

    // 음수 값을 양수 값으로 변환
    if (RollValue < 0)
    {
        RollValue += 360.0f;
    }

    // 각도 범위에 따라 숫자를 계산
    int PositionIndex = FMath::RoundToInt(RollValue / 36.0f) % 10;
    return static_cast<float>(PositionIndex);
}


C++에서 특정 블루프린트 액터 가져오기

문제

  • GetActorOfClass 함수로 특정 블루프린트 액터를 가져오려 했으나, 원하는 인덱스의 액터를 가져올 수 없었습니다. 항상 첫 번째 액터만 가져오는 문제가 발생했습니다.

해결법

  • GetActorOfClass 함수를 통해 간단하게 특정 클래스의 액터를 가져올 수 있으나 특정 인덱스의 액터를 가져오는 기능이 부족하기 때문에 GetAllActorsOfClass를 사용하기로 했습니다.

  • GetAllActorsOfClass로 모든 액터를 가져와서 인덱스로 몇 번째 액터를 조회할 것인지 만들었습니다.

ASlidingDoorActor* GetNthActorOfClass(TSubclassOf<AActor> ActorClass, int32 N)
{
    TArray<AActor*> FoundActors;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), ActorClass, FoundActors);
    if (FoundActors.IsValidIndex(N))
    {
        return Cast<ASlidingDoorActor>(FoundActors[N]);
    }
    return nullptr;
}

블루프린트에서 애니메이션 블루프린트 변수 변경

원인과 결과

  • 원인: 애니메이션 블루프린트의 변수에 접근하여 변경하는 방법을 찾지 못함.
  • 결과: 애니메이션 블루프린트에서 상태 변화를 제어할 수 없는 문제 발생.

장단점

  • 장점: 애니메이션 블루프린트 변수를 통해 캐릭터 상태를 정교하게 제어 가능.
  • 단점: 올바른 애니메이션 인스턴스 캐스팅 문제로 인한 코드 실행 불가.

해결법

#include "MyAnimInstance.h" // UMyAnimInstance 클래스를 포함합니다

void ACombinationLockActor::BeginPlay()
{
    Super::BeginPlay();

    if (SkeletalMesh)
    {
        UAnimInstance* AnimInstance = SkeletalMesh->GetAnimInstance();
        if (AnimInstance)
        {
            UMyAnimInstance* MyAnimInstance = Cast<UMyAnimInstance>(AnimInstance);
            if (MyAnimInstance)
            {
                MyAnimInstance->bIsRunning = true;
            }
            else
            {
                UE_LOG(LogTemp, Warning, TEXT("AnimInstance is not of type UMyAnimInstance"));
            }
        }
        else
        {
            UE_LOG(LogTemp, Warning, TEXT("SkeletalMesh does not have an AnimInstance"));
        }
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("SkeletalMesh is null"));
    }
}

선택한 칸을 표시하는 방법

자물쇠에서 현재 선택된 칸을 시각적으로 표시하기 위해 두 가지 방법을 생각해냈습니다.

  1. 레이캐스트 사용:

    • 캐릭터에서 레이캐스트를 쏴서 해당 칸에 부딪치면 회전이 가능해지고 문을 열 수 있게 하고자 했습니다.
    • 하지만 캐릭터 클래스에 로직이 추가되므로, 이미 많은 내용을 포함하고 있는 캐릭터 클래스를 복잡하게 만들 수 있어 피하고자 했습니다.
  2. 인덱스 기반 배열 관리:
    - 인덱스를 만들어서 각 칸들을 관리하는 배열을 선택하게끔 했습니다.
    - 특정 키를 눌러 칸을 지정하고 드래그할 수 있도록 구현했습니다.
    - 매시의 매테리얼을 가져와서 BaseColor의 알파값을 조절하는 방법이 있고 메시의 크기를 조금 키우는 방법이 있습니다.

    매시의 매테리얼을 가져와서 변경
    메시의 크기를 조금 키우기

시각적으로 후자가 더 좋다고 판단하여 선택된 칸의 크기를 키우는 방법으로 구현했습니다.

코드 예시

void ACombinationLockActor::SelectWheelMesh(int32 Index)
{
    if (SelectedWheelIndex != Index)
    {
        // 이전 선택된 매시의 크기를 원래대로 돌림
        WheelMeshArray[SelectedWheelIndex]->SetRelativeScale3D(FVector::One() * DefaultScale);

        // 새로운 매시를 선택하고 크기를 키움
        SelectedWheelIndex = Index;
        WheelMeshArray[SelectedWheelIndex]->SetRelativeScale3D(FVector::One() * SelectedScale);
    }
}

이번 작업을 통해 조합 자물쇠 로직을 완성하고, 문과의 상호작용을 구현하며, 코드의 유지보수성을 높일 수 있었습니다. 앞으로도 지속적으로 개선해 나갈 계획입니다.

참고 자료


이와 같이 작업하면서 많은 것을 배웠고, 이를 통해 프로젝트의 완성도를 높일 수 있었습니다. 앞으로도 꾸준히 개선하여 더 나은 결과를 만들어 나가겠습니다.

profile
게임 프로그래머

0개의 댓글