[UE5] Lyra의 Crouch 카메라 보정

2024. 2. 18. 15:50강의/UE5 기타 강의들

- Lyra는 카메라를 CameraComponent를 커스텀해서, CameraMode 클래스를 통해 카메라를 조절합니다. 이에 관한 글은 따로 작성할 것이고, 이 글에서는 Lyra에서 웅크리기 모션을할 시 카메라를 보정하는 방법에 대해 다룹니다.

 

- 또한, 이 글은 Lyra의 코드와 몇몇 강의를 보고 제가 이해한 대로 정리한 글이기 때문에 정확성이 떨어질 수 있습니다.

 

 

1. 웅크리기 보정?

 

웅크리기시 부드럽게 카메라 또한 낮아지는 것을 볼 수 있습니다.

 

- Lyra에서 카메라는 CameraArm 과 같은 컴포넌트를 사용하지 않고, CameraMode를 통해 구현합니다.

- 따라서, 캐릭터가 위처럼 웅크리기를 수행할 때 부드럽게 카메라를 이동시키는 것을 직접 구현해야합니다.

 

 

2. Pivot Location?

Pivot!

 

- Pivot 이란? "어떠한 대상의 중심축" 을 이야기합니다. 보통 삼인칭 게임에서는 우리의 캐릭터가 Pivot이 됩니다.

- Lyra에서는 Camera의 Pivot Location을 CameraMode::GetPivotLocation 에서 구현하여, Pivot의 위치를 가져옵니다.

- 주요 로직은 아래와 같습니다.

// CameraMode::GetPivotLocation
const AActor* TargetActor = GetTargetActor();

// 만약, Character를 타겟으로 삼는다면.. 웅크리기와 같은 movement component에 대응
if (const ACharacter* TargetCharacter = Cast<ACharacter>(TargetPawn))
    return 고정한 Pivot Location

// 만약, Pawn이라면, Pawn의 View 즉, ActorLocation + eye height 반환
if (const APawn* TargetPawn = Cast<APawn>(TargetActor))
    return TargetPawn->GetPawnViewLocation();
    
// 만약, Actor라면 그냥 Actor의 위치 반환
return TargetActor->GetActorLocation();

 

- Character, Pawn, Actor를 부모로 삼는지 확인 후 적절한 위치를 가져오는 것을 볼 수 있죠

- 여기서, Pawn은 PawnViewLocation을 통해 머리의 위치, 즉 눈의 위치를 가져와 우리가 흔히 보는 3인칭 카메라의 PivotLocation을 정할 수 있습니다.

 

- 하지만, PawnViewLocation을 PivotLocation으로 사용한다면 어떻게 될까요?

 

view location을 피벗으로 정하면..... 카메라가, 순간이동하는 것 처럼

 

FVector APawn::GetPawnViewLocation() const
{
	return GetActorLocation() + FVector(0.f,0.f,BaseEyeHeight);
}

 

- TargetPawn->GetPawnViewLocation()을 들여다보면 BaseEyeHeight 이라는 값과 ActorLocation을 더한 것임을 알 수 있습니다.

 

// ACharacter.cpp ... Crouch 상태이면, CrouchedEyeHeight으로 BaseEyeHeight이 설정됨
void ACharacter::RecalculateBaseEyeHeight()
{
	if (!bIsCrouched)
	{
		Super::RecalculateBaseEyeHeight();
	}
	else
	{
		BaseEyeHeight = CrouchedEyeHeight;
	}
}

 

 

- 하지만, BaseEyeHeight, GetActorLocation() 의 값들은 웅크리기시 변하는 값입니다. 그러므로 순간이동한 것 처럼 카메라가 이동하게됩니다. 

 

- 사용자가(CameraMode를 상속받아서 사용하는) 부드럽게 카메라를 움직이는 기능을 구현하게 하려면, 이 PivotLocation을 고정시키는 방법밖에 없습니다.

 

 

2.1 Pivot Location 고정하는 방법?

최상위 root component는 Capsule

 

- GetActorLocation은 RootComponent의 Location을 가져옵니다. 

- 보통 ACharacter 를 상속받는 클래스들은 CapsuleComponent의 Location입니다.

 

 

- Lyra에서 이 Pivot Location을 고정하는 방법은 캡슐의 Half Height를 이용하였습니다. 

 

 

- 웅크리기시 중심의 Location은 위와 같이 줄어든 사이즈의 절반만큼 이동하게됩니다.

- 따라서, 이 값을 구하여 더해주면, GetActorLocation의 변화에 대응할 수 있습니다.

- 또한, 캐릭터의 CDO의 BaseEyeHeight을 가져오면, eye height 또한 고정시킬 수 있습니다.

 

- 최종적으로, Lyra의 PivotLocation을 고정시키는 코드는 다음과 같습니다.

 

		// Height adjustments for characters to account for crouching.
		if (const ACharacter* TargetCharacter = Cast<ACharacter>(TargetPawn))
		{
			const ACharacter* TargetCharacterCDO = TargetCharacter->GetClass()->GetDefaultObject<ACharacter>();
			check(TargetCharacterCDO);

			const UCapsuleComponent* CapsuleComp = TargetCharacter->GetCapsuleComponent();
			check(CapsuleComp);

			const UCapsuleComponent* CapsuleCompCDO = TargetCharacterCDO->GetCapsuleComponent();
			check(CapsuleCompCDO);

			const float DefaultHalfHeight = CapsuleCompCDO->GetUnscaledCapsuleHalfHeight();
			const float ActualHalfHeight = CapsuleComp->GetUnscaledCapsuleHalfHeight();
			const float HeightAdjustment = (DefaultHalfHeight - ActualHalfHeight) + TargetCharacterCDO->BaseEyeHeight;

			return TargetCharacter->GetActorLocation() + (FVector::UpVector * HeightAdjustment);
		}

 

 

- 이제, 사용자 코드인 CameraMode_ThirdPerson 에서 PivotLocation을 가져와 웅크리기시, 부드럽게 Location을 변화시켜야합니다.

 

 

2.2 참고 

	// OnStartCrouch takes the change from the Default size, not the current one (though they are usually the same).
	const float MeshAdjust = ScaledHalfHeightAdjust;
	ACharacter* DefaultCharacter = CharacterOwner->GetClass()->GetDefaultObject<ACharacter>();
	HalfHeightAdjust = (DefaultCharacter->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight() - ClampedCrouchedHalfHeight);
	ScaledHalfHeightAdjust = HalfHeightAdjust * ComponentScale;

 

- 위 코드는 UCharacterMovementComponent의 Crouch 함수에 있는 코드입니다.

- 여기서, Crouch 상태가 되면 Height를 GetUnScaledCapsuleHalfHeight를 사용하여 조정하는 것을 확인할 수 있습니다.

 

 

3. EyeHeight를 CameraMode에서 보간 수행

 

- CameraMode는 매 프레임마다 UpdateView 메서드를 실행하여, 카메라의 위치, 각도 등을 조정합니다. 따라서, 이 함수에서 캐릭터가 웅크리는 것을 확인한 후, GetPivotLocation으로 가져온 PivotLocation의 높이를 줄여나가면 됩니다.

void ULyraCameraMode_ThirdPerson::UpdateView(float DeltaTime)
{
	UpdateForTarget(DeltaTime);
	UpdateCrouchOffset(DeltaTime);
    
	FVector PivotLocation = GetPivotLocation() + CurrentCrouchOffset;

 

3.1 UpdateForTarget

void ULyraCameraMode_ThirdPerson::UpdateForTarget(float DeltaTime)
{

	if (const ACharacter* TargetCharacter = Cast<ACharacter>(GetTargetActor()))
	{
		if (TargetCharacter->bIsCrouched)
		{
			const ACharacter* TargetCharacterCDO = TargetCharacter->GetClass()->GetDefaultObject<ACharacter>();
			const float CrouchedHeightAdjustment = TargetCharacterCDO->CrouchedEyeHeight - TargetCharacterCDO->BaseEyeHeight;

			SetTargetCrouchOffset(FVector(0.f, 0.f, CrouchedHeightAdjustment));

			return;
		}
	}

	SetTargetCrouchOffset(FVector::ZeroVector);
}

 

- Lyra에서는 위와 같이 Character의 bIsCrouched를 사용하여, 웅크리기시 TargetOffset을 설정합니다.

 

PRAGMA_DISABLE_OPTIMIZATION
void ULyraCameraMode_ThirdPerson::SetTargetCrouchOffset(FVector NewTargetOffset)
{
	CrouchOffsetBlendPct = 0.0f;
	InitialCrouchOffset = CurrentCrouchOffset;
	TargetCrouchOffset = NewTargetOffset;
}
PRAGMA_ENABLE_OPTIMIZATION

 

- Pct는 Lerp를 수행할 때 사용되는 Alpha 값입니다. 각 변수는 다음과 같이 사용됩니다.

 

FMath::InterpEaseInOut(InitialCrouchOffset, TargetCrouchOffset, CrouchOffsetBlendPct, 1.0f);

 

3.2 UpdateForTarget  문제점? (5.3 기준)

 

-  UpdateForTarget CrouchOffsetBlendPct가 매 프레임마다 초기화된다는 것입니다.

- 역시 같은 문제점을 지적한 PR을 찾아 볼 수 있었습니다. 

- https://github.com/EpicGames/UnrealEngine/pull/10546

- 따라서, 저는 제 프로젝트에서는  CrouchOffsetBlendPct를 그냥 제거하는 방식으로 블렌딩하였습니다.

 

 

3.3 UpdateCrouchOffset

void ULyraCameraMode_ThirdPerson::UpdateCrouchOffset(float DeltaTime)
{
	if (CrouchOffsetBlendPct < 1.0f)
	{
		CrouchOffsetBlendPct = FMath::Min(CrouchOffsetBlendPct + DeltaTime * CrouchOffsetBlendMultiplier, 1.0f);
		CurrentCrouchOffset = FMath::InterpEaseInOut(InitialCrouchOffset, TargetCrouchOffset, CrouchOffsetBlendPct, 1.0f);
	}
	else
	{
		CurrentCrouchOffset = TargetCrouchOffset;
		CrouchOffsetBlendPct = 1.0f;
	}
}

 

- 매 프레임마다, CrouchOffsetBlendPct를 업데이트하며, Init 에서 Target으로 적절히 보간하는 것으로 부드럽게 카메라 Pivot location에 더해줄 값을 설정해줍니다.

 

웅크리기시 부드럽게 카메라가 이동하는 것을 볼 수 있습니다.

결과는 위와 같습니다. 

4. 후기

- CapsuleComponent를 이용하여, 위치를 보정할 수 있는 방법을 알 수 있었습니다.

- Lyra의 Camera Mode 이해도를 높일 수 있었습니다.

- BaseEyeHeight과 CapsuleHeight을 구하는 부분은 CDO 사용의 좋은 예시인 것 같다고 생각하였습니다.

- Lyra에서도 잘못된 코드가 있음을 알게되었고, 항상 의심하는 습관을 가져야할 것 같다고 느꼈습니다.