언리얼을 다루다 보면 함수 이름이 직관적으로 받아 들여지는 느낌과 달라서 직관 따로 이성 따로 움직이는 경우가 많다. 그 중 대표주자가 SetViewTarget 이다(라고 생각한다). 함수 이름을 처음 보면 바라보는 목표 지점을 설정하는거 같은데 전혀 아니다(필자가 콩글리쉬가 강해서 그럴지도 모른다).
이번 포스팅에서는 SetViewTarget 을 머리에서 어떻게 변환해서 생각할 지와 도대체 왜 AActor Class 에 GetActorEyesViewPoint(...) 가 있는지 생각해보자. 그리고 UnrealEngine 에서 각종 View를 계산할때 어느 시점을 기준으로 CameraLocation, CameraRotation 을 가져 오는지 알아보고 FollowingCamera 를 어떻게 설정하면 좋을지도 다뤄 본다.
이번 포스팅에서는 다음은 자세히 다루지 않고 기본적인 내용만 다루기로 한다.

최종적으로 결정되어야 하는 값은 APlayerCameraManager::CameraCachePrivate 이다. 이 값을 기준으로 각종 View 를 계산하게 된다. 결국 이 포스팅은 CameraCache 를 계산하는 과정에 대한 설명이 된다. FMinimalViewInfo 에 있는 Location 에서 Rotation 으로 머리를 돌리고 시야각 FOV 로 카메라가 설정된다고 생각하면 된다. APlayerCameraManager 에게 Location, Rotation 을 물어보는 것은 결국 CameraCache 에서 값을 꺼내 가는게 된다.
FRotator APlayerCameraManager::GetCameraRotation() const
{
return GetCameraCacheView().Rotation;
}
FVector APlayerCameraManager::GetCameraLocation() const
{
return GetCameraCacheView().Location;
}
void APlayerCameraManager::GetCameraViewPoint(FVector& OutCamLoc, FRotator& OutCamRot) const
{
const FMinimalViewInfo& CurrentPOV = GetCameraCacheView();
OutCamLoc = CurrentPOV.Location;
OutCamRot = CurrentPOV.Rotation;
}
대략적인 구조를 살펴보자.
이중 CameraStyle 은 APlayerCameraManager::UpdateViewTarget 안에 다음과 같이 정의 되어 있다. CameraStyle 의 Default 값은 NAME_Default 이며 아래에 해당하지 않는다. CameraStyle 이 지정되면 약간 다르게 동작하게 되는데 그건 직접 UpdateViewTarget Code 를 여러분이 직접 살펴보자.
Camera Style Name
static const FName NAME_Fixed = FName(TEXT("Fixed"));
static const FName NAME_ThirdPerson = FName(TEXT("ThirdPerson"));
static const FName NAME_FreeCam = FName(TEXT("FreeCam"));
static const FName NAME_FreeCam_Default = FName(TEXT("FreeCam_Default"));
static const FName NAME_FirstPerson = FName(TEXT("FirstPerson"));
자 그럼 시작해 보자
UE_5.4/Engine/Source/Runtime/Engine/Private/LevelTick.cpp
void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
{
...
RunTickGroup(TG_PrePhysics);
...
RunTickGroup(TG_StartPhysics);
...
RunTickGroup(TG_DuringPhysics, false);
...
RunTickGroup(TG_PostPhysics);
...
PlayerController->UpdateCameraManager(DeltaSeconds)
...
}
UE_5.4/Engine/Source/Runtime/Engine/Private/PlayerController.cpp
void APlayerController::UpdateCameraManager(float DeltaSeconds)
{
if (PlayerCameraManager != NULL)
{ PlayerCameraManager->UpdateCamera(DeltaSeconds);
}
}
World::Tick(...) 이 실제로 모든 Actor Tick 을 돌린다고 생각해도 된다. 위에 나오는 RunTickGroup 들은 각 Tick(ActorTick, ComponentTick)이 속한 Group 을 작동 시킨다. 여기서 중요한 점은 AActor 의 기본 TickGroup 은 PrimaryActorTick.TickGroup = TG_PrePhysics; 이고 AActorComponent 의 TickGorup 은 PrimaryComponentTick.TickGroup = TG_DuringPhysics; 이라는 점이다. TickGroup 은 다른 포스팅에서 다뤄 보기로 하자.
따라서 UpdateCamera 는 모든 Actor 가 작동(Tick) 을 수행하고 TickGroup 소속을 넘어서 맨 마지막에 작동한다. 이 점이 아주 중요한데, Unity 를 다뤄 보신 분들이라면 Following Camera 를 만들때 카메라가 부들부들 떨리는걸 본 경험들이 있을 거다. 10중 8,9는 Update,FixedUpdate 주기 문제 이거나 어느 Component, 어느 GameObject 가 먼저 수행하느냐에 따라서 발생하게 된다. 즉, Camera 가 바라보는 Object 좌표 갱신 이 먼저냐 Camera 위치, 방향 계산이 먼저냐 에 따라서 생기는 일이 대부분이다.
하지만 위와 같이 Unreal 에서는 APlayerCameraManager 를 사용할 것을 반 강제하고 있고 계산 Timing 이 모든 Actor 연산 이후로 고정되어 있어서 카메라가 떨릴 일이 없다. 그런데 Camera 가 떨린다면 여러분은 뭔가를 잘 못하고 있는 것이다.
언리얼에서 작업자가 작업후 Camera 가 떨린다고 해서 살펴보면, 대부분 CameraActor 를 만들어 두고 해당 CameraActor 을 SetViewTarget 으로 설정한 다음, CameraActor 의 Tick 에서 위치, 방향을 계산하는 경우가 상당 수다. 정 CameraActor 를 따로 만들고 해당 CameraActor 가 특정 Actor 를 바라보게 하겠다면 다른 함수를 활용해야 한다.
위의 해답은 이후 다시 생각해 보기로 하고 일단은 Code 를 따라가 보자.
APlayerCameraManager::UpdateCamera 의 작동은 사전 검사할 것을 확인한 후 DoUpdateCamera(...) 를 호출 하는 것이다.
void APlayerCameraManager::UpdateCamera(float DeltaTime)
{
check(PCOwner != nullptr);
if ((PCOwner->Player && PCOwner->IsLocalPlayerController()) || !bUseClientSideCameraUpdates || bDebugClientSideCamera)
{
DoUpdateCamera(DeltaTime);
...
void APlayerCameraManager::DoUpdateCamera(float DeltaTime)
{
FMinimalViewInfo NewPOV = ViewTarget.POV;
...
UpdateViewTarget(ViewTarget, DeltaTime);
...
NewPOV = ViewTarget.POV;
if (PendingViewTarget.Target != NULL)
{
...
UpdateViewTarget(PendingViewTarget, DeltaTime);
...
NewPOV = ViewTarget.POV;
NewPOV.BlendViewInfo(PendingViewTarget.POV, BlendPct);
...
}
...
FillCameraCache(NewPOV);
}
DoUpdateCamera 의 주 역할은 현재 ViewTarget 으로 설정된 View를 갱신하고, 필요하면 전환 중(목표)인 PendingViewTarget 의 View도 계산하고 보간한 다음 CameraCache 에 저장 하는것이다.
아래에서 다시 언급 하겠지만 ViewTarget 은 카메라가 바라보는 곳이 아니다. 카메라의 위치,방향(FMinimalViewInfo) 을 채우는데 있어 어느 Actor(ViewTarget.Target) 에게 물어볼 것인가가 지정되어 있다. 그 값을 채우는 UpdateViewTarget 을 이어서 살펴보자.
void APlayerCameraManager::UpdateViewTarget(FTViewTarget& OutVT, float DeltaTime)
{
...
if (ACameraActor* CamActor = Cast<ACameraActor>(OutVT.Target))
{
// Viewing through a camera actor.
CamActor->GetCameraComponent()->GetCameraView(DeltaTime, OutVT.POV);
}
else
{
...
else if (CameraStyle == NAME_FirstPerson)
{
// Simple first person, view through viewtarget's 'eyes'
OutVT.Target->GetActorEyesViewPoint(OutVT.POV.Location, OutVT.POV.Rotation);
// don't apply modifiers when using this debug camera mode
bDoNotApplyModifiers = true;
}
else
{
UpdateViewTargetInternal(OutVT, DeltaTime);
}
}
}
우선 살펴 볼것은 OutVT(ViewTarget)의 Target 이 ACameraActor 계열일 때다. CameraActor 를 상속받아서 만들면 위와 같이 CameraComponent 를 땡겨와서 해당 Component 에 물어본다. 아래에 있는 CameraStyle 로 아예 들어가질 않는다.
현재 쫓아가고 있는 함수들은 APlayerCameraManager 의 ViewTarget, PendingViewTarget 모두 호출 되는 곳이라는 걸 잊지 말자. 즉, ViewTarget, PendingViewTarget 이 뭐든지 간에 물어보는 방식은 일단 APlayerCameraManager 가 결정한다.
CameraStyle == NAME_FirstPerson등은 아래 내용을 살펴본 다음 여러분이 직접 살펴보기로 하자. 만약 Custom 한 CameraStyle 을 지정했거나, 아무런 지정을 하지 않으면 결국 UpdateViewTargetInternal 로 들어간다.
void APlayerCameraManager::UpdateViewTargetInternal(FTViewTarget& OutVT, float DeltaTime)
{
if (OutVT.Target)
{
FVector OutLocation;
FRotator OutRotation;
float OutFOV;
if (BlueprintUpdateCamera(OutVT.Target, OutLocation, OutRotation, OutFOV))
{
OutVT.POV.Location = OutLocation;
OutVT.POV.Rotation = OutRotation;
OutVT.POV.FOV = OutFOV;
}
else
{
OutVT.Target->CalcCamera(DeltaTime, OutVT.POV);
}
}
}
만약 APlayerCameraManager 를 상속받아 Blueprint 를 만들고, PlayerController 에게 해당 BP를 사용하게 지정해두고 BlueprintUpdateCamera 를 구현해 두었다면, 해당 BP 에 묻고 끝난다.
아니면 드디어 해당 TargetActor 에게 CalcCamera 를 요청한다.
만약 Custom CameraStyle 을 구현하겠다면 UpdateViewTargetInternal 을 재정의 하는게 가장 편해 보인다.
void AActor::CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult)
{
if (bFindCameraComponentWhenViewTarget)
{
// Look for the first active camera component and use that for the view
TInlineComponentArray<UCameraComponent*> Cameras;
GetComponents(/*out*/ Cameras);
for (UCameraComponent* CameraComponent : Cameras)
{
if (CameraComponent->IsActive())
{
CameraComponent->GetCameraView(DeltaTime, OutResult);
return;
}
}
}
GetActorEyesViewPoint(OutResult.Location, OutResult.Rotation);
}
void APlayerController::CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult)
{
OutResult.Location = GetFocalLocation();
OutResult.Rotation = GetControlRotation();
}
CalcCamera 는 무려 최상위 Class 라고 할 수 있는 AActor 에 기본 구현되어 있고, 재정의 하고 있는 곳은 APlayerController 가 대표적이다.
AActor::CalcCamera(...) 의 경우 기본 true 인 bFindCameraComponentWhenViewTarget 을 확인하고 아래 Component 중 UCameraComponent 를 찾고 있으면 해당 CameraComponent 에게 GetCameraView 를 요청한다. TP_Person Template 예제에서 3인칭 카메라 값이 결정되는 곳이 이곳이다.
bFindCameraComponentWhenViewTarget 이 false 이거나, CameraComponent 가 없으면 GetActorEyesViewPoint 로 들어간다.
따로 재정의 되어 있지 않다면 기본적으로, 해당 Actor의 Location, Rotation 을 사용한다.
void AActor::GetActorEyesViewPoint( FVector& OutLocation, FRotator& OutRotation ) const
{
OutLocation = GetActorLocation();
OutRotation = GetActorRotation();
}
Target 이 Pawn계열이라면
void APawn::GetActorEyesViewPoint( FVector& out_Location, FRotator& out_Rotation ) const
{
out_Location = GetPawnViewLocation();
out_Rotation = GetViewRotation();
}
FVector APawn::GetPawnViewLocation() const
{
return GetActorLocation() + FVector(0.f,0.f,BaseEyeHeight);
}
Target 이 Character 계열이면 추가적으로 웅크린 상태를 확인해서 EyeHeight 을 조정한다.
void ACharacter::RecalculateBaseEyeHeight()
{
if (!bIsCrouched)
{
Super::RecalculateBaseEyeHeight();
}
else
{
BaseEyeHeight = CrouchedEyeHeight;
}
}
AActor 계열은 World 에 존재하는 Object들의 최고 상위 Class 이다. UCameraComponent 도 있고 기본적으로 ACameraActor 도 제공 되는데 왜 최상위 Class 에 저 함수가 있을까?
몇가지 이유를 추측해 볼 수 있는데, 지금부터는 완전히 필자 개인 추측이며 공식적으로 언급된적이 있는지는 찾아 보지 못했다.
첫째는 우선 다른 포스팅에서도 언급 되겠지만 UnrealEngine 이 원래 Unreal Tournament(이하 UT) Code 에서 분리 발전 되어 왔다는 것이다. UT 는 FPS 게임이고 기본적인 카메라는 내가 조종하는 캐릭터의 눈 과 내 캐릭터가 죽었을때 다른 캐릭터의 눈, 또는 자유롭게 날아다니는 Spectator 의 눈 이다. UT가 처음 나왔을때 카메라는 저거면 족했고, 카메라의 대상이 되는 움직이는 객체도 딱 저정도 였을 것이다.
둘째는 언리얼이 Component 기반 으로 시작한 엔진이 아니라는 점이다. 이는 다른 포스팅에서 자세히 다뤄 본다.
셋째는 현재 방식이 아주 유도리가 있다는 점이다. 처음에 보면 일견 복잡해 보이고 번거러워 보일 수 있는데, 사실 APlayerCameraManager 에게 일임 함으로서 얻을 수 있는 잇점이 상당히 많다. 예를들어 특정 캐릭터 주위를 빙글빙글 돌게 만드는 카메라를 만든다고 했을때, 다른 Class 수정없이 APlayerCameraManager 에 기능 추가로 할 수도 있다(물론 Camera역할 Actor를 돌릴 수도 있을 것이다). CalcCamera 의 경우 PlayerController 에서도 재정의 하고 있는데, 카메라 방향과 위치를 따로 제어할때 사용되기도 한다.
그럼 특정 캐릭터를 따라 다니는 Following Camera 를 만드는 방법에 대해 생각해 보자. 꼭 위치까지 따라 다니는게 아니라, 특정 위치에서 고개만 돌리는 방법도 고려해 보자.
등등등... 여기서 중요한건 ViewTarget Actor 의 Rotation 에 의존하지 말고, TargetActor의 CalcCamera 를 재정의 하거나, APlayerCameraManager 에서 UpdateViewTarget_Internal 등을 계산할떄 바라볼 Target 을 구해서 그 시점에 Rotation 을 계산해야 한다는 것이다.
APlayerController 에서 Class 를 지정해 주면 된다.
/** PlayerCamera class should be set for each game, otherwise Engine.PlayerCameraManager is used */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=PlayerController)
TSubclassOf<APlayerCameraManager> PlayerCameraManagerClass;
AMyPlayerController::AMyPlayerController(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PlayerCameraManagerClass = AMyPlayerCameraManager::StaticClass();
}
SetViewTarget 은 누구에게 카메라 계산을 맡길 것인가 정하는 함수이다.