캐릭터의 이동과 카메라 회전을 구현하는 와중, 카메라와 스프링암의 움직임, 이들의 컨트롤 옵션에 따른 변화를 인지하기 위해 작성하는 글. 공부하면서 작성하는 글이기 때문에, 혹시라도 보고 계신 분들이 있다면... 틀릴 수도 있다는 것을 인지하고 읽어주시면 감사하겠습니다!
void APlayer::Look(const FInputActionValue& Value)
{
FVector2D Data = Value.Get<FVector2D>();
AddControllerPitchInput(Data.Y);
AddControllerYawInput(Data.X);
}
이 Look
함수에 의해, 마우스 움직임에 따라 SpringArm이 회전하는 것처럼 연출된다. AddControllerPitchInput, AddControllerYawInput
이 함수들 안에서 무슨 일이 일어나고 있는걸까? Yaw 함수를 타고 들어가봤다.
void APawn::AddControllerYawInput(float Val)
{
if (Val != 0.f && Controller && Controller->IsLocalPlayerController())
{
APlayerController* const PC = CastChecked<APlayerController>(Controller);
PC->AddYawInput(Val);
}
}
값이 0이 아니고, 컨트롤러가 있을 때 플레이어 컨트롤러로 캐스팅 한 후 컨트롤러에 AddYawInput(Val)
을 호출해주고 있다.
void APlayerController::AddYawInput(float Val)
{
RotationInput.Yaw += !IsLookInputIgnored() ? Val * (GetDefault<UInputSettings>()->bEnableLegacyInputScales ? InputYawScale_DEPRECATED : 1.0f) : 0.0f;
}
여기서 RotationInput.Yaw
에 들어온 Val값을 더해주고 있다는 것을 알 수 있다. 그럼 RotationInput은 어디서 쓰일까?
플레이어 컨트롤러 코드에서 읽고있는 것을 보니, 저쪽에서 사용하고 있는 것 같다. 들어가보자.
void APlayerController::UpdateRotation( float DeltaTime )
{
// Calculate Delta to be applied on ViewRotation
FRotator DeltaRot(RotationInput);
FRotator ViewRotation = GetControlRotation();
if (PlayerCameraManager)
{
PlayerCameraManager->ProcessViewRotation(DeltaTime, ViewRotation, DeltaRot);
}
AActor* ViewTarget = GetViewTarget();
if (!PlayerCameraManager || !ViewTarget || !ViewTarget->HasActiveCameraComponent() || ViewTarget->HasActivePawnControlCameraComponent())
{
if (IsLocalPlayerController() && GEngine->XRSystem.IsValid() && GetWorld() != nullptr && GEngine->XRSystem->IsHeadTrackingAllowedForWorld(*GetWorld()))
{
auto XRCamera = GEngine->XRSystem->GetXRCamera();
if (XRCamera.IsValid())
{
XRCamera->ApplyHMDRotation(this, ViewRotation);
}
}
}
SetControlRotation(ViewRotation);
APawn* const P = GetPawnOrSpectator();
if (P)
{
P->FaceRotation(ViewRotation, DeltaTime);
}
}
RotationInput은 플레이어 컨트롤러의 UpdateRotation
에서 뷰 회전의 변화량을 나타내기 위한 FRotator를 만들 때 사용하고 있다. ProcessViewRotation
을 통해 뷰 회전값을 보정한(뷰 회전 제한을 적용한다던지) 후, 그 값을 사용해 SetControlRotation
을 해주고 있다.
void AController::SetControlRotation(const FRotator& NewRotation)
{
if (!IsValidControlRotation(NewRotation))
{
logOrEnsureNanError(TEXT("AController::SetControlRotation attempted to apply NaN-containing or NaN-causing rotation! (%s)"), *NewRotation.ToString());
return;
}
if (!ControlRotation.Equals(NewRotation, 1e-3f))
{
ControlRotation = NewRotation;
if (RootComponent && RootComponent->IsUsingAbsoluteRotation())
{
RootComponent->SetWorldRotation(GetControlRotation());
}
}
else
{
//UE_LOG(LogPlayerController, Log, TEXT("Skipping SetControlRotation %s for %s (Pawn %s)"), *NewRotation.ToString(), *GetNameSafe(this), *GetNameSafe(GetPawn()));
}
}
이 안에서는 ControlRotation 값을 설정해주고, IsUsingAbsoluteRotation
값이 true일 경우 루트 컴포넌트(캐릭터로 따지면 아마도 Capsule Component)의 월드 회전값을 설정해준다. 결론적으로, 액터의 루트 컴포넌트의 월드 회전을 컨트롤러의 회전과 동일하게 설정한다는 의미이다.
IsUsingAbsoluteRotation
이 변수가 true로 설정되면 캐릭터가 회전할 때 스프링 암이 회전하지 않는다. 정확하게는, 해당 액터나 컴포넌트의 회전이 부모의 회전에 영향을 받지 않고 항상 절대적인 값을 유지하도록 하는 변수이다.
만약 Pawn이 있다면, 월드 회전값을 설정해 준 뒤엔 Pawn의 FaceRotation
함수를 호출해준다.
void APawn::FaceRotation(FRotator NewControlRotation, float DeltaTime)
{
// Only if we actually are going to use any component of rotation.
if (bUseControllerRotationPitch || bUseControllerRotationYaw || bUseControllerRotationRoll)
{
const FRotator CurrentRotation = GetActorRotation();
if (!bUseControllerRotationPitch)
{
NewControlRotation.Pitch = CurrentRotation.Pitch;
}
if (!bUseControllerRotationYaw)
{
NewControlRotation.Yaw = CurrentRotation.Yaw;
}
if (!bUseControllerRotationRoll)
{
NewControlRotation.Roll = CurrentRotation.Roll;
}
#if ENABLE_NAN_DIAGNOSTIC
if (NewControlRotation.ContainsNaN())
{
logOrEnsureNanError(TEXT("APawn::FaceRotation about to apply NaN-containing rotation to actor! New:(%s), Current:(%s)"), *NewControlRotation.ToString(), *CurrentRotation.ToString());
}
#endif
SetActorRotation(NewControlRotation);
}
}
Pawn의 컨트롤 옵션인 bUseControllerRotationPitch, Yaw, Roll
의 값에 따라 Actor의 로테이션을 설정해준다. 만약 true일 경우, 폰의 회전은 Control Rotation 회전 값을 그대로 따라가게 됨!
지금까지 내가 이해한 내용을 정리해보겠다. 마우스를 통해 값이 들어오면 컨트롤러의 회전값이 되고, bUseControllerRotationPitch, Yaw, Roll
의 값이 반영되어 최종적으로 액터의 회전값이 결정된다.
이제 컨트롤러의 값이 어떻게 액터의 회전에 반영되는지 알았으니, 스프링암의 움직임을 쫓아보자.
void USpringArmComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
UpdateDesiredArmLocation(bDoCollisionTest, bEnableCameraLag, bEnableCameraRotationLag, DeltaTime);
}
Tick 함수부터 타고 내려가보기로 했다. UpdateDesiredArmLocation
함수를 보자. 먼저 GetTargetRotation
함수를 통해 DesiredRot을 가져온다.
FRotator USpringArmComponent::GetTargetRotation() const
{
FRotator DesiredRot = GetDesiredRotation();
if (bUsePawnControlRotation)
{
if (APawn* OwningPawn = Cast<APawn>(GetOwner()))
{
const FRotator PawnViewRotation = OwningPawn->GetViewRotation();
if (DesiredRot != PawnViewRotation)
{
DesiredRot = PawnViewRotation;
}
}
}
// If inheriting rotation, check options for which components to inherit
if (!IsUsingAbsoluteRotation())
{
const FRotator LocalRelativeRotation = GetRelativeRotation();
if (!bInheritPitch)
{
DesiredRot.Pitch = LocalRelativeRotation.Pitch;
}
if (!bInheritYaw)
{
DesiredRot.Yaw = LocalRelativeRotation.Yaw;
}
if (!bInheritRoll)
{
DesiredRot.Roll = LocalRelativeRotation.Roll;
}
}
return DesiredRot;
}
bUsePawnControlRotation
이 true일 경우, 폰의 뷰 회전값을 가져오게 되는데 만약 스프링암의 회전값이 폰의 회전값과 다르다면 DesiredRot를 PawnViewRotation과 같게 설정해준다.
액터가 절대 회전값을 사용하지 않을 때는 상대적인 회전값을 받아와 bInheritPitch/Yaw/Roll
의 값에 따라 DesiredRot를 설정해준다. bInheritPitch가 false라면, 로컬 회전값을 그대로 사용하게 되는 것.
GetRelativeRotation()
해당 액터나 컴포넌트의 부모를 기준으로 한 상대적인 회전값을 반환한다.
이렇게 회전값을 받아온 후엔, 여러가지 옵션값을 통해 값을 조정해주는 것 같다. 이부분은 넘어가도 될 것 같아 생략한다.
// Update socket location/rotation
RelativeSocketLocation = RelCamTM.GetLocation();
RelativeSocketRotation = RelCamTM.GetRotation();
마지막으로, 소켓의 상대적인 위치와 회전값을 설정해주는 것 같다. 아마 카메라의 상대적인 위치를 담아두는 변수 같은데, 카메라에서 이 값을 가져와 트랜스폼을 설정하지 않을까? 이제 카메라 컴포넌트로 이동해보자.
void UCameraComponent::GetCameraView(float DeltaTime, FMinimalViewInfo& DesiredView)
{
// ...
if (bUsePawnControlRotation)
{
const APawn* OwningPawn = Cast<APawn>(GetOwner());
const AController* OwningController = OwningPawn ? OwningPawn->GetController() : nullptr;
if (OwningController && OwningController->IsLocalPlayerController())
{
const FRotator PawnViewRotation = OwningPawn->GetViewRotation();
if (!PawnViewRotation.Equals(GetComponentRotation()))
{
SetWorldRotation(PawnViewRotation);
}
}
}
// ...
}
bUsePawnControlRotation
이 true일 경우, Pawn의 뷰 회전을 가져와서 월드 회전값을 설정해준다. 스프링암의 옵션과 동일하게, Pawn의 회전값과 일치시키는듯 하다.
카메라에서 따로 소켓의 위치를 가져오는 것 같진 않아서, 다시 스프링암으로 돌아가 찾아보니 GetSocketTransform
이라는 함수에서 사용하고 있는 것을 알 수 있었다. 소켓의 트랜스폼을 계산하기 위해 RelativeSocketLocation / Rotation 변수를 사용하고 있는 것 같다.
뭔가 이상한 곳으로 빠진 것 같지만, 코드 구현할 때 유용하게 사용할 수 있을 것 같아서 알아두려고 조금 자세하게(?) 알아봤다.
마지막으로 캐릭터 무브먼트 컴포넌트의 Use Controller Desired Rotation
과 Orient Rotation To Movement
를 알아볼 차례이다. 코드를 전부 분석하진 못했고, 공부한 내용만 적어두려고 한다. 힘들어서.. 나중에 하면 수정해야지..
Use Controller Desired Rotation
true일 경우, 컨트롤러 값에 따라 액터를 회전시킨다. 액터의 이동 방향은 회전값과 관계 없이 유지된다.
=> 그니까 마우스 회전에 따라 액터는 회전하는데 내가
w키를 누르면 액터의 전방 방향으로, a키를 누르면 왼쪽 방향으로 움직이는건 동일하다는 소리인 것 같다.
Orient Rotation To Movement
true일 경우, 액터가 이동할 때 이동 방향을 향해 회전한다. 캐릭터의 이동 방향을 시각적으로 파악하기 쉽게 해준다.