구현해야 하는 카메라 조작은 이렇게 나뉜다.
이동중 회전
정지 상태에서 회전
Pawn
Use Controller Rotation Pitch
Use Controller Rotation Yaw
Use Controller Rotation Roll
Idle 상태에서 Yaw 회전 90 이하까지는 캐릭터 머리만 회전, 이상일 경우 Turn 애니메이션 출력되며 캐릭터 방향 전환
애니메이션 때문에 AnimInstance에서 캐스팅한 후 플레이어의 현재 상태 받아올 수 있어야 함
정지 / 이동중 회전 + ALT (카메라만 회전)
Pawn
Use Controller Rotation Pitch
Use Controller Rotation Yaw
Use Controller Rotation Roll
Move 함수를 바꿔야 함
ALT 놨을 때 캐릭터가 바라보는 방향으로 카메라 이동 -> 보간해야 함
컨트롤 옵션은 데이터 에셋을 사용해 런타임중에 자동적으로 바뀌도록 만들어줄 생각이었는데, 생각보다 변경해야 할 값이 아직은 많이 없는 것 같아서 일단 Enum값에 따라 바뀌도록 할 생각이다.
UENUM(BlueprintType)
enum class ECharacterControlType : uint8
{
Moving UMETA(DisplayName = "Moving"),
MovingAlt UMETA(DisplayName = "MovingAlt"),
Idle UMETA(DisplayName = "Idle"),
IdleAlt UMETA(DisplayName = "IdleAlt"),
};
Input Action 하나를 생성해주고, IMC에 추가해주었다. Triggers를 Released로 설정해준 뒤 SetupPlayerInputComponent
함수에 바인딩 해줬다.
UIC->BindAction(CameraFixAction, ETriggerEvent::Started, this, &ATrapperPlayer::CameraFixStart);
UIC->BindAction(CameraFixAction, ETriggerEvent::Completed, this, &ATrapperPlayer::CameraFixEnd);
이렇게 키를 눌렀을 때와 뗐을 때 각각 한번씩만 호출되도록 설정해주고, 각각의 함수에서는 bIsAltPressed
변수를 true와 false값으로 바꿔주게 했다.
void ATrapperPlayer::CharacterControlTypeCheck()
{
ECharacterControlType TempType = ControlState;
if (GetVelocity().IsNearlyZero())
{
bIsAltPressed ? ControlState = ECharacterControlType::IdleAlt : ControlState = ECharacterControlType::Idle;
}
else
{
bIsAltPressed ? ControlState = ECharacterControlType::MovingAlt : ControlState = ECharacterControlType::Moving;
}
// 만약 상태가 달라진다면 함수 호출
if (TempType != ControlState)
{
SetCharacterControlData();
}
}
void ATrapperPlayer::SetCharacterControlData()
{
switch (ControlState)
{
case ECharacterControlType::Moving:
bUseControllerRotationYaw = true;
break;
case ECharacterControlType::MovingAlt:
bUseControllerRotationYaw = false;
break;
case ECharacterControlType::Idle:
bUseControllerRotationYaw = false;
break;
case ECharacterControlType::IdleAlt:
bUseControllerRotationYaw = false;
break;
default:
break;
}
}
그리고 캐릭터의 컨트롤 타입을 체크하는 함수 하나를 만들어서, Velocity 값이 0에 수렴하고 Alt키가 눌려있다면 Idle Alt 상태로, 안눌려있다면 Idle 상태로, Velocity가 0이 아닐경우 Alt키가 눌려있다면 Moving Alt 상태로, 안눌려있다면 Moving 상태로 전환해주었다. 이걸 매 Tick마다 체크하도록 두는게 맞는지는 고민이 좀 더 필요할 것 같다. 캐릭터들의 상태가 하나씩 추가할 때마다 더 좋은 구조를 고민해볼 예정이다. 아무튼, 상태가 바뀔 때마다 컨트롤 옵션 값을 바꿔주는 코드까지 구현해주었다.
이제 Alt를 누르고 달리면 카메라만 돌아가고, 누르지 않고 달리면 캐릭터도 함께 돌아가는 상태가 됐다. 하지만 Move 함수가 컨트롤 회전값을 받아 캐릭터를 움직이고 있어서 캐릭터가 의도대로 움직이지 않는다. Alt키가 눌린 상태에서는 별도로 동작하도록 바꿔주어야 한다.
void ATrapperPlayer::Move(const FInputActionValue& Value)
{
FVector2D Data = Value.Get<FVector2D>();
FRotator Rotation;
if(ControlState == ECharacterControlType::MovingAlt) Rotation = GetActorRotation();
else Rotation = GetControlRotation();
const FRotator ForwordRotation = FRotator(0, Rotation.Yaw, 0);
const FVector ForwordVector = UKismetMathLibrary::GetForwardVector(ForwordRotation);
const FVector RightVector = UKismetMathLibrary::GetRightVector(ForwordRotation);
AddMovementInput(ForwordVector, Data.Y);
AddMovementInput(RightVector, Data.X);
}
원래는 이것도 IMC를 하나 더 만들어서 하려고 했는데, 바꿀게 별로 없을 것 같아 Enum의 상태값에 따라 받아오는 Rotation을 다르게 설정해주었다.
Alt키를 눌렀을 때 카메라만 회전되도록 변경된 모습.
기획서에 카메라 관련 수치가 업데이트되어 Spring Arm과 Camera의 값을 변경해 주었다.
Alt를 누르고 달리다 Alt를 해제하면, 카메라가 캐릭터가 바라보고 있는 곳을 향해 회전해야 한다.
처음엔 Alt를 눌렀을 때, 스프링암의 로테이션 값을 기억해두고 나중에 현재 스프링암의 로테이션 과 기억해둔 값을 보간해서 쓰려고 했는데.. 이것저것 테스트하다보니 Alt를 누르면 스프링암이 컨트롤러 로테이션에 영향을 받지 않아서 그런건지 아예 멈춰있게 된다는 것을 알게 되었다.
그래서 카메라의 이전 값을 저장하고 현재값과 보간해 자연스럽게 움직이려고 해봤더니, 카메라가 이상한 곳으로 이동함과 동시에 스프링암의 움직임과 이상하게 엮여 움직이는 현상이 발생했다.
카메라의 이전 값은 이상할 수밖에 없었다. Alt를 눌렀을 때의 월드 트랜스폼을 저장하면, 캐릭터가 가만히 있는게 아니기 때문에 소용이 없는거였다. 그래서 그 값으로 카메라를 이동시키면 캐릭터를 한참 멀리서 쳐다보는 현상들이 나왔던 것 같다.
스프링암과의 상대적인 위치를 사용해 해결할 수 있을 것 같다. 내가 정리한 글에 따로 적어두었지만, 스프링암에 달려있는 소켓의 트랜스폼을 가져오는 GetSocketTransform
함수가 있었다.
처음 실행됐을 때의 스프링암에 대한 카메라의 상대적인 위치를 저장해주고, 보간할 때 타겟을 스프링암의 위치에서 초반의 상대적 위치를 더해서 만들어주면 되지 않을까?
// Camera Test
FTransform ArmRelativeTransform = SpringArm->GetRelativeTransform();
UE_LOG(LogTemp, Warning, TEXT("Arm Relative Transform : %s"), *ArmRelativeTransform.ToString());
FTransform ArmWorldTransform = SpringArm->GetComponentTransform();
UE_LOG(LogTemp, Warning, TEXT("Arm World Transform : %s"), *ArmWorldTransform.ToString());
FTransform SoketRelativeTransform = SpringArm->GetSocketTransform(USpringArmComponent::SocketName, RTS_Component);
UE_LOG(LogTemp, Warning, TEXT("Socket Relative Transform : %s"), *SoketRelativeTransform.ToString());
FTransform SoketWorldTransform = SpringArm->GetSocketTransform(USpringArmComponent::SocketName, RTS_World);
UE_LOG(LogTemp, Warning, TEXT("Socket World Transform : %s"), *SoketWorldTransform.ToString());
FTransform CameraRelativeTransform = Camera->GetRelativeTransform();
UE_LOG(LogTemp, Warning, TEXT("Camera Relative Transform : %s"), *CameraRelativeTransform.ToString());
FTransform CameraWorldTransform = Camera->GetComponentTransform();
UE_LOG(LogTemp, Warning, TEXT("Camera World Transform : %s"), *CameraWorldTransform.ToString());
우선 이 셋의 관계에 대해 이해하기 위해 로그를 전부 찍어보았다.
소켓과 카메라의 World Transform이 일치하는 것을 확인했다. 카메라가 움직일 때 스프링암이 움직이는게 아니라 소켓이 움직이는듯 하다. 카메라의 월드 트랜스폼만 따로 Set하면 소켓과 카메라간의 위치가 달라지게 된다. 근데.. 소켓 자체를 움직이는 방법이 없는 것 같다. 외부에서 변경되지 않도록 설정되어 있는 것 같음.
컨트롤러의 입력이 들어갈 때 호출되는 함수인 AddControllerYawInput
으로 조절하는 수밖에 없는 것 같다. 소켓의 Relative Rotation이 0이 될 때까지 값을 줘보는 식으로 해보자.
// Tick함수 (임시)
FRotator SoketRelativeRotation = SpringArm->GetSocketTransform(USpringArmComponent::SocketName, RTS_Component).Rotator();
SoketRelativeRotation.Normalize();
UE_LOG(LogTemp, Warning, TEXT("Socket Relative Rotation : %s"), *SoketRelativeRotation.ToString());
if (!bActorRotationInterpEnd)
{
float SmoothFactor = 0.002f;
float SmoothPitchInput = SoketRelativeRotation.Pitch * SmoothFactor;
float SmoothYawInput = SoketRelativeRotation.Yaw * SmoothFactor;
if (FMath::IsNearlyZero(SoketRelativeRotation.Pitch) && FMath::IsNearlyZero(SoketRelativeRotation.Yaw))
{
bActorRotationInterpEnd = true;
}
else
{
AddControllerPitchInput(-SmoothPitchInput);
AddControllerYawInput(-SmoothYawInput);
}
}
우선 이렇게 구현해봤다. 생각했던 움직임과 유사하게 움직이긴 하나, Pitch값이 자꾸 -89.9, 89.9에 가서 붙는다... Yaw는 정확하게 동작하는데, 도대체 뭐가 문제일까? 오일러각 문제도 아닌 것 같은데..
혹시나 해서 Pitch값을 빼고 Yaw값만 입력되도록 바꿔줬는데, 생각보다 훨씬 자연스럽고 동작도 잘 되는 것을 확인..!!! 비록 앞의 문제는 아직 이유를 모르겠어서 언젠가 다시 해결해봐야겠지만 기획자분과 얘기해서 Yaw값만 돌아가도록 합의(?)했다. 조작감도 괜찮으시다구 합격 주셔서!! 일단 이대로 진행하기로 했다!
void ATrapperPlayer::TurnInterp()
{
if (bActorRotationInterpEnd) return;
FRotator SoketRelativeRotation = SpringArm->GetSocketTransform(USpringArmComponent::SocketName, RTS_Component).Rotator();
SoketRelativeRotation.Normalize();
float SmoothYawInput = SoketRelativeRotation.Yaw * SmoothFactor;
const float Tolerance = 0.1f;
if (FMath::IsNearlyEqual(SoketRelativeRotation.Yaw, 0.f, Tolerance))
{
bActorRotationInterpEnd = true;
}
else
{
AddControllerYawInput(-SmoothYawInput);
}
}
TurnInterp
함수로 따로 빼주고 Tick에서 호출하도록 정리했고, SmoothFactor
변수는 기획자분들이 조절할 수 있도록 블루프린트에 노출시켜줬다.