[UE5] Lena: Dev Diary #12 - 버튼 자물쇠 구현과 최적화

ChangJin·2024년 7월 11일
0

Unreal Engine5

목록 보기
78/102
post-thumbnail

2024.07.11

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

이번 포스팅에서는 ButtonLockActor을 구현하면서 버튼 자물쇠를 만드는 방법을 다룹니다. 로직 구현, 타임라인 사용법, TArray 활용, USTRUCT, UCLASS로 데이터 관리의 차이점, PlayerController로 카메라 이동 등을 중점적으로 설명합니다.

진행상황

  • ✅ 조합 자물쇠
    • ✅ 각 칸 회전 정의
    • ✅ 문과의 상호작용 구현
    • ✅ 타임라인을 사용한 회전 처리
  • ✅ 방향 자물쇠
    • ✅ 방향 입력 정의
    • ✅ 문과의 상호작용 구현
    • ✅ 타임라인을 사용한 이동 처리
  • ✅ 버튼 자물쇠
    • ✅ 카메라 이동 구현
    • ✅ 버튼의 이동 클래스 데이터, TArray 구현
  • ⬜ 여러 자물쇠 비밀번호 관리
버튼 자물쇠 관계도
업데이트 된 상호작용 관련 상속 관계도

만들고자 하는 자물쇠

버튼 자물쇠 구현

이번 작업에서는 버튼 자물쇠의 로직을 구현했습니다. 버튼 자물쇠는 사용자가 특정 버튼을 눌러 자물쇠를 풀 수 있도록 설계되었습니다.

1. 문자열 결합 문제

메시를 여러 개 만들 때 문자열 결합으로 이름을 생성하면 문제가 발생할 수 있습니다. FString::Printf 함수를 사용하여 안전하게 문자열 형식화를 처리해야 합니다.

TEXT에 int32를 더하면 이상한 결과가 나옵니다. 이러한 현상이 발생하는 이유는 C++의 문자열 결합과 Unreal Engine의 TEXT 매크로의 사용 방식의 차이입니다.

기존 코드

int32 Counter = 1;
for (UStaticMeshComponent*& ButtonLockButtonComponent : ButtonLockButtonMeshComponents)
{
    ButtonLockButtonComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ButtonLockButtonComponent" + Counter++));
    ButtonLockButtonComponent->SetupAttachment(ButtonLockBodyMeshComponent);
}

이 부분에서 "ButtonLockButtonComponent" 문자열에 Counter 정수를 더하려고 시도하고 있습니다. 그러나 C++에서 문자열 리터럴과 정수를 직접 더할 수 없기 때문에, 컴파일러는 이를 허용하지 않습니다. 대신, 포인터 연산처럼 해석되어 이상한 결과가 나타날 수 있습니다.

"ButtonLockButtonComponent" + Counter는 "ButtonLockButtonComponent" 문자열의 시작 주소에 Counter 값을 더하는 포인터 연산으로 해석됩니다. 따라서, 문자열의 일부를 건너뛰고 접근하게 되어 이상한 문자열이 생성됩니다.

문제 해결 코드

int32 Counter = 1;
for (UStaticMeshComponent*& ButtonLockButtonComponent : ButtonLockButtonMeshComponents)
{
    FString ComponentName = FString::Printf(TEXT("ButtonLockButtonComponent_%d"), Counter++);
    ButtonLockButtonComponent = CreateDefaultSubobject<UStaticMeshComponent>(*ComponentName);
    ButtonLockButtonComponent->SetupAttachment(ButtonLockBodyMeshComponent);
}

문자열과 정수를 결합하려면, FString 클래스를 사용해야 합니다. FString::Printf 함수 또는 FString 생성자 등을 이용해 문자열을 조합할 수 있습니다.

2. 카메라 이동

카메라를 원하는 위치로 이동하기 위해 APlayerController를 상속받은 클래스 부분에서 PlayerCameraManagerSetViewTargetWithBlend를 사용했습니다. 카메라를 특정 위치로 이동시키기 위해 버튼자물쇠의 앞에 임시로 AActor를 생성하고, 해당 위치로 이동하게 만들었습니다.

폰에 빙의된 카메라의 위치
버튼 자물쇠 앞에 생성된 액터의 위치

카메라 이동 구현

ButtonLock에서 임시 AActor를 생성

FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
CameraTargetActor = GetWorld()->SpawnActor<AActor>(AActor::StaticClass(), SpawnParams);

if (CameraTargetActor)
{
    USceneComponent* SceneComponent = NewObject<USceneComponent>(CameraTargetActor);
    if (SceneComponent)
    {
        CameraTargetActor->SetRootComponent(SceneComponent);
        SceneComponent->RegisterComponent();
        SceneComponent->SetWorldLocation(ZoomedCameraLocation);
        SceneComponent->SetWorldRotation(ZoomedCameraRotation);
    }
}

여기서 NewObject를 쓰는 이유는 무엇인가?

  • NewObject를 사용하는 이유는 CreateDefaultSubobject가 액터의 생성자 내부에서만 사용 가능한 함수이기 때문입니다. SpawnActor로 생성된 액터에 서브오브젝트를 추가하려면 생성자 외부에서 사용할 수 있는 NewObject 또는 AddComponent를 사용해야 합니다.

CreateDefaultSubobject vs NewObject

CreateDefaultSubobject는 액터의 생성자 내부에서만 사용됩니다. 액터가 생성될 때 기본적으로 서브오브젝트를 추가하는 데 사용됩니다.

// 액터 생성자 내부
UStaticMeshComponent* MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
NewObject:

NewObject는 액터의 생성자 외부에서도 사용 가능합니다. 런타임 동안 동적으로 서브오브젝트를 추가하는 데 사용됩니다.

// 액터 생성자 외부
	USceneComponent* SceneComponent = NewObject<USceneComponent>(CameraTargetActor);

APlayerController에서 PlayerCameraManager를 사용

PlayerCameraManager = PlayerController->PlayerCameraManager;
if (PlayerController)
{
    if (PlayerCameraManager)
    {
        InitialCameraLocation = PlayerCameraManager->GetCameraLocation();
        InitialCameraRotation = PlayerCameraManager->GetCameraRotation();

        ZoomedCameraLocation = GetActorLocation() + GetActorForwardVector() * 200.0f + FVector(0.f, 0.f, 50.f);
        ZoomedCameraRotation = GetActorRotation();

        FActorSpawnParameters SpawnParams;
        SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
        CameraTargetActor = GetWorld()->SpawnActor<AActor>(AActor::StaticClass(), SpawnParams);

        if (CameraTargetActor)
        {
            USceneComponent* SceneComponent = NewObject<USceneComponent>(CameraTargetActor);
            if (SceneComponent)
            {
                CameraTargetActor->SetRootComponent(SceneComponent);
                SceneComponent->RegisterComponent();
                SceneComponent->SetWorldLocation(ZoomedCameraLocation);
                SceneComponent->SetWorldRotation(ZoomedCameraRotation);
            }
        }
    }
}

3. 버튼 자물쇠 데이터 관리

버튼 자물쇠의 상태를 관리하기 위해 데이터 클래스를 정의했습니다. 구조체로 시도했으나 디테일 패널에서 정보가 보이지않았습니다. Unreal Engine에서는 UCLASS로 전환해야 컴포넌트 관리를 쉽게 할 수 있습니다.

데이터 클래스 정의

#pragma once

#include "CoreMinimal.h"
#include "DataStructure.generated.h"

USTRUCT(BlueprintType)
struct FButtonLockButtonData
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    bool bIsButtonMove;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    bool IsCliked;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FVector InitialLocation;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FVector TargetLocation;

    FButtonLockButtonData()
        : bIsButtonMove(false)
        , IsCliked(false)
        , InitialLocation(FVector::ZeroVector)
        , TargetLocation(FVector::ZeroVector)
    {
    }
};

4. 버튼 자물쇠 액터 구현

ButtonLockActor에서 버튼 데이터 클래스를 활용하여 각 버튼의 상태를 관리하고, 타임라인을 사용하여 버튼이 눌리는 애니메이션을 구현했습니다.

ButtonLockActor.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ButtonLockButtonData.h"
#include "ButtonLockActor.generated.h"

UCLASS()
class YOURPROJECT_API AButtonLockActor : public AActor
{
    GENERATED_BODY()

public:
    AButtonLockActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
    
    ... // body, Shackle등 다른 메시들
    
    UPROPERTY(VisibleAnywhere, Category="ButtonLock")
	TArray<UStaticMeshComponent*> ButtonMeshComponents;

	UPROPERTY(VisibleAnywhere, Category="ButtonLock")
	TArray<UTimelineComponent*> ButtonTimelines;

    UPROPERTY(EditAnywhere, Category="ButtonLock")
    TArray<UButtonLockButtonData*> ButtonDataArray;

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

    UFUNCTION(BlueprintCallable, Category="ButtonLock")
    void MoveButton(UStaticMeshComponent* TargetMeshComponent);

    UFUNCTION()
    void HandleButtonLockProgress(float value, int32 ButtonIndex);

    UFUNCTION()
    void HandleButtonLockFinished(int32 ButtonIndex);

private:
    void InitializeButtonData();
};

ButtonLockActor.cpp

#include "ButtonLockActor.h"
#include "Components/TimelineComponent.h"
#include "Kismet/GameplayStatics.h"

AButtonLockActor::AButtonLockActor()
{
    PrimaryActorTick.bCanEverTick = true;
    ButtonTimelines.SetNum(8);
    ButtonDataArray.SetNum(8);
}

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

    for (int32 i = 0; i < ButtonDataArray.Num(); ++i)
    {
        if (ButtonTimelines[i])
        {
            FOnTimelineFloat TimelineProgress;
            TimelineProgress.BindUFunction(this, FName("HandleButtonLockProgress"), i);
            ButtonTimelines[i]->AddInterpFloat(ButtonLockCurve, TimelineProgress);

            FOnTimelineEvent TimelineFinished;
            TimelineFinished.BindUFunction(this, FName("HandleButtonLockFinished"), i);
            ButtonTimelines[i]->SetTimelineFinishedFunc(TimelineFinished);
        }
    }
}

void AButtonLockActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

void AButtonLockActor::MoveButton(UStaticMeshComponent* TargetMeshComponent)
{
    int32 FindIndex = ButtonDataArray.IndexOfByPredicate([TargetMeshComponent](const UButtonLockButtonData* Data)
    {
        return Data->ButtonMeshComponent == TargetMeshComponent;
    });

    if (FindIndex != INDEX_NONE && !ButtonDataArray[FindIndex]->bIsButtonMove)
    {
        ButtonDataArray[FindIndex]->InitialLocation = TargetMeshComponent->GetRelativeLocation();
        ButtonDataArray[FindIndex]->TargetLocation = ButtonDataArray[FindIndex]->InitialLocation + FVector(0, -10, 0);
        ButtonDataArray[FindIndex]->bIsButtonMove = true;
        ButtonTimelines[FindIndex]->PlayFromStart();
    }
}

void AButtonLockActor::HandleButtonLockProgress(float value, int32 ButtonIndex)
{
    if (ButtonIndex != INDEX_NONE && ButtonDataArray.IsValidIndex(ButtonIndex))
    {
        FVector NewLocation = FMath::Lerp(ButtonDataArray[ButtonIndex]->InitialLocation, ButtonDataArray[ButtonIndex]->TargetLocation, value);
        ButtonMeshComponent[ButtonIndex]->SetRelativeLocation(NewLocation);
    }
}

void AButtonLockActor::HandleButtonLockFinished(int32 ButtonIndex)
{
    if (ButtonIndex != INDEX_NONE && ButtonDataArray.IsValidIndex(ButtonIndex))
    {
        ButtonDataArray[ButtonIndex]->bIsButtonMove = false;
    }
}

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

타임라인을 사용하여 버튼이 눌리는 애니메이션을 구현했습니다. 타임라인의 진행 상태에 따라 버튼의 위치를 업데이트하는 콜백 함수를 정의했습니다.

FOnTimelineEvent에 매개변수를 전달하여 각 버튼의 Callback 함수를 등록해줄 수 있지 않을까라고 생각했습니다.

그러나 언리얼 엔진에서는 FOnTimelineEvent를 사용하여 타임라인 이벤트를 바인딩할 때 매개변수를 전달할 수 없습니다. 이는 FOnTimelineEvent의 설계 방식에 기인합니다. FOnTimelineEvent는 매개변수가 없는 델리게이트를 기대하며, 따라서 매개변수가 있는 함수를 직접 바인딩할 수 없습니다.

즉, CreateUFunction이나 BindUFunction을 사용하여 FOnTimelineEvent에 매개변수를 전달하려고 시도하면 컴파일 오류가 발생합니다. 이를 해결하려면 매개변수를 필요로 하지 않는 다른 방식을 사용해야 합니다. 예를 들어, 람다 함수를 사용하거나, 매개변수를 전역 변수나 클래스 멤버 변수로 관리하는 방법이 있습니다.

타임라인이 종료될 때 특정 인덱스를 전달하고 싶다면, 별도의 관리 구조체나 클래스를 사용하여 필요한 데이터를 저장하고 참조하는 방식으로 우회할 수 있습니다.

이게 안된다는게 뭐가 문제냐면... 버튼 자물쇠에서 1번 버튼을 눌렀을 때 이 버튼이 들어가는 동안 다른 버튼을 누를 수 있어야 합니다. 그러나 FOnTimelineEvent를 다 다르게 등록하지 않으면 1번 버튼이 움직이는 동안 다른 버튼들은 이동할 수 없습니다. 지금은 버튼 자물쇠의 이동 시간이 매우 짧아서 이렇게 만들어도 문제가 없지만 이동시간이 매우 긴 컴포넌트를 어떻게 관리할지에 대한 논의가 충분히 필요해보입니다.

UTimelineComponent* ButtonTimeline;

UPROPERTY()
FOnTimelineFloat ButtonTimelineCallback;

UPROPERTY()
FOnTimelineEvent ButtonTimelineFinishedCallback;

UFUNCTION()
void Handle

ButtonLockProgress(float value);

UFUNCTION()
void HandleButtonLockFinished();

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

bool bIsTimelinePlaying = false;

6. PlayerController 설정

PlayerController 클래스에서 향상된 입력 시스템을 사용하여 마우스 클릭 이벤트를 처리했습니다.

PlayerController 설정

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category= "Input")
UInputAction* IA_MouseClick;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Input")
UInputMappingContext* IMC_Interaction;

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

    UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
    if (Subsystem)
    {
        Subsystem->AddMappingContext(IMC_Interaction, 0);
    }

    if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(InputComponent))
    {
        EnhancedInputComponent->BindAction(IA_MouseClick, ETriggerEvent::Triggered, this, &AYourPlayerController::HandleMouseClick);
    }
}

마우스 클릭 핸들러

void AYourPlayerController::HandleMouseClick()
{
    if (!bIsClickEnabled) return;

    float MouseX, MouseY;
    GetMousePosition(MouseX, MouseY);

    FVector2D ScreenPosition(MouseX, MouseY);
    FVector WorldLocation, WorldDirection;
    if (DeprojectScreenPositionToWorld(ScreenPosition.X, ScreenPosition.Y, WorldLocation, WorldDirection))
    {
        FVector Start = WorldLocation;
        FVector End = WorldLocation + (WorldDirection * 1000.0f);

        FHitResult HitResult;
        FCollisionQueryParams Params;
        Params.AddIgnoredActor(this);

        if (GetWorld()->LineTraceSingleByChannel(HitResult, Start, End, ECC_Visibility, Params))
        {
            if (HitResult.GetActor())
            {
                AButtonLockActor* ButtonLockActor = Cast<AButtonLockActor>(HitResult.GetActor());
                if (ButtonLockActor)
                {
                    UStaticMeshComponent* ButtonMeshComponent = Cast<UStaticMeshComponent>(HitResult.GetComponent());
                    ButtonLockActor->MoveButton(ButtonMeshComponent);
                }
            }
        }
        DrawDebugLine(GetWorld(), Start, End, FColor::Green, false, 1.0f, 0, 1.0f);
    }
}

블렌더로 자물쇠 3D 모델링

버튼 자물쇠의 3D 모델링을 Blender로 직접 수정하여 Unreal Engine에 적용했습니다. Blender에서 자물쇠의 각 부분을 나누어 제작한 후, fbx 파일로 내보내기 하여 Unreal Engine에서 컴포넌트로 조립했습니다.

홈을 파주기
Text -> ConvertMesh -> 붙여주기
각 버튼 1~8번 만들어주기

결론

이번 작업을 통해 버튼 자물쇠 로직을 완성하고, 각 버튼의 상태를 관리하며, 타임라인을 사용해 컴포넌트를 이동시켰습니다. 데이터 클래스를 활용하여 코드의 유지보수성을 높였으며, 여러 문제 상황을 해결하면서 많은 것을 배웠습니다. 또한, Blender를 통해 3D 모델링을 직접 제작하여 Unreal Engine에 적용하는 과정을 경험했습니다.

현재는 블루프린트로 간단하게 자물쇠와 문을 연동하고 있는습니다. 다음 작업은 여러 자물쇠의 비밀번호를 효율적으로 관리하고, 각 자물쇠와 문을 연동하는 방법을 다룰 예정입니다. 그리고 데이터 셋을 사용하여 비밀번호와 힌트 등을 더욱 효율적으로 관리하는 방법을 학습할 예정입니다.

참고 자료


profile
Unreal Engine 클라이언트 개발자

0개의 댓글