UE5/Lyra Clone Coding

[UE5] Camera

검정색필통 2025. 2. 5. 18:04

Lyra의 Camera의 궁극적인 목표는 결국 Camera Component의 모듈화이다.

CameraManager(CameraManager 상속), CameraComponent(CameraComponent 상속), CameraMode(UObject 상속)를 생성하는 것으로 시작한다.

 

 

CameraManager

 

CameraManager에서는 FOV(시야각), PITCH_MAX, PITCH_MIN(상하로 회전하는 각도)의 기본값을 설정해준다.

AZSPlayerCameraManager::AZSPlayerCameraManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
	DefaultFOV = ZS_CAMERA_DEFAULT_FOV;
	ViewPitchMin = ZS_CAMERA_DEFAULT_PITCH_MIN;
	ViewPitchMax = ZS_CAMERA_DEFAULT_PITCH_MAX;
}

 

이 CameraManager는 PlayerController가 관리한다.

 

 

CameraMode

 

Camera의 모드는 여러 개가 있을 수 있다. 예를 들어 FPS의 기본 모드, 스나이퍼 모드, 차량에 탔을 때의 카메라 모드 등 상태에 따라 카메라가 변경되어야 할 수 있다.

 

따라서 Lyra에서는 여러 개의 CameraMode를 저장하기 위해 CameraModeStack이란 개념을 만들었다.

이 Stack은 두 가지의 역할을 하는 데,

1. 모드별로 구분이 가능하니 카메라의 모듈성, 확장성을 만들 수 있다.

2. 현재 모드에서 다른 모드로 넘어갔을 때 한번에 넘어가게 되면 부자연스러울 수 있는데, 카메라 모드 스택을 통해 카메라 상태를 내부적으로 저장해서 자연스럽게 바꿀 수 있도록 한다. (카메라의 텔레포트 방지)

 

 

CameraComponent

 

이 CameraStack은 CameraComponent에서 관리하고 호출한다.

void UZSCameraComponent::OnRegister()
{
	Super::OnRegister();

	if (!CameraModeStack)
	{
		// 초기화 (BeginPlay와 같은)가 딱히 필요없는 객체로 NewObject로 할당
		CameraModeStack = NewObject<UZSCameraModeStack>(this);
	}
}

카메라 모드 기본 스택은 Null이지만 onRegister, 즉 카메라 컴포넌트가 캐릭터에게 부착이 되는 단계에서 카메라 모드 스택을 만들게 된다.

 

Camera는 PlayerController가 관리한다

 

카메라는 기본적으로 월드에 있는 Tick을 따라가는 게 아니라 PlayerController의 UpdateCamera를 통해서 CameraManager에서 업데이트를 진행하게된다.

 

이 CameraManager가 CalculationCamera 다음에 GetCameraView를 매 프레임 호출함으로써 보고 있는 시야각을 정한다.

 

CameraManager와 CameraComponent의 차이가 좀 헷갈릴 수 있는데

캐릭터가 움직이면 보이는 시야에 따라 카메라가 화면을 담는데 그것을 찍는 역할은 CameraComponent이지만,

그 정보를 이용해서 어떻게 렌더링해서 어떤 결과로서 출력을 할 것인지, 이것을 종합적으로 관리하는 것은 CameraManager이다.

 

Camera Component는 Character에  종속되어있는 컴포넌트이다. 플레이어가 다른 캐릭터로 전환할 때 Camera의 렌더링이 Character에 종속되어 있다면 카메라 전환이 어려워진다. 즉, PlayerController가 변경이 되었을 때를 고려하여 Character가 아닌 PlayerController에 종속적인 CameraManager를 둔것이다.

 

void UZSCameraComponent::GetCameraView(float DeltaTime, FMinimalViewInfo& DesiredView)
{
	check(CameraModeStack);

	UpdateCameraModes();
}

void UZSCameraComponent::UpdateCameraModes()
{
	check(CameraModeStack);

	// DetermineCameraModeDelegate는 CameraMode Class를 반환한다:
	// - 해당 delegate는 HeroComponent의 멤버 함수로 바인딩되어 있다
	if (DetermineCameraModeDelegate.IsBound())
	{
		if (const TSubclassOf<UZSCameraMode> CameraMode = DetermineCameraModeDelegate.Execute())
		{
			CameraModeStack->PushCameraMode(CameraMode);
		}
	}
}

 

GetGameraView함수는 매 프레임 호출되고, 그로 인해 UpdateCameraModes 함수에서 CameraMode를 CameraStack에 매 프레임 저장하게 된다.

 

이후 CameraMode_ThirdPerson이라는 CameraMode를 상속받는 블루브린트 클래스를 만들어 PawnData에 적용해주고 기본 카메라 세팅을 마친다.

 

 

이제 CameraMode에서 Camera 인스턴스를 갖고오거나 생성해야한다.

UZSCameraMode* UZSCameraModeStack::GetCameraModeInstance(TSubclassOf<UZSCameraMode>& CameraModeClass)
{
	check(CameraModeClass);

	// CameraModeInstances에서 먼저 생성되어있는지 확인 후, 반환한다:
	for (UZSCameraMode* CameraMode : CameraModeInstances)
	{
		// CameraMode는 UClass를 비교한다:
		// - 즉, CameraMode는 클래스 타입에 하나만 생기게된다
		if ((CameraMode != nullptr) && (CameraMode->GetClass() == CameraModeClass))
		{
			return CameraMode;
		}
	}

	// CameraModeClass에 알맞는 CameraMode의 인스턴스가 없다면 생성한다:
	UZSCameraMode* NewCameraMode = NewObject<UZSCameraMode>(GetOuter(), CameraModeClass, NAME_None, RF_NoFlags);
	check(NewCameraMode);

	// 여기서 우리는 CameraModeInstances는 CameraModeClass에 맞는 인스턴스를 가지고(관리하는) 컨테이너이라는 것을 알 수 있다
	CameraModeInstances.Add(NewCameraMode);

	return NewCameraMode;
}

 

이제 우리가 블루프린트에서 설정해준 카메라 모드를 갖고와야 하는데, 한 게임에서 필요한 카메라 모드의 수는 그렇게 많지는 않다. 따라서 갖고올 때마다 인스턴스를 새로 생성해준다면 큰 낭비라고 할 수 있다.

 

그래서 사용 하는 것이 CameraModeInstances다.

두 배열의 차이는?

둘다 같은 타입의 포인터객체를 담고 있지만, 사용하는 곳은 다르다.

CameraModeStack은 카메라 전환을 매끄럽게 하기 위해 업데이트 진행하는 스택에 담아두는 것이고,

CameraModeInstances는 카메라 전환시에 효율성을 위해 인스턴스를 새로 생성하지 않고 캐싱해서 사용하기 위해 만들어진 배열이다.

 

이제 여러 카메라 모드들이 전환될 때 어떻게 변환되는지 알아볼 차례이다.

 

그런데 이 카메라 전환을 다루는 블렌딩(Blending)은 꽤 설명하기 복잡하다... 따라서 추후에 게시글을 쓰던 설명은 미루도록하고, 대략적으로 요약하면 CameraModeStack에 카메라 모드들을 담아두고 스택에 넣었다 빼었다 하면서 가중치를 두어 부드럽게 카메라 전환될 수 있도록 한다고 이해하면 될 것 같다.

 

 

CameraView

 

이제 카메라 뷰를 갖고오는 함수를 살펴보자.

void UZSCameraComponent::GetCameraView(float DeltaTime, FMinimalViewInfo& DesiredView)
{
	check(CameraModeStack);

	UpdateCameraModes();

	FZSCameraModeView CameraModeView;
	CameraModeStack->EvaluateStack(DeltaTime, CameraModeView);

	if (APawn* TargetPawn = Cast<APawn>(GetTargetActor()))
	{
		if (APlayerController* PC = TargetPawn->GetController<APlayerController>())
		{
			PC->SetControlRotation(CameraModeView.ControlRotation);
		}
	}

	// Camera의 Location과 Rotation을 반영하자:
	SetWorldLocationAndRotation(CameraModeView.Location, CameraModeView.Rotation);

	// FOV 업데이트
	FieldOfView = CameraModeView.FieldOfView;

	 // FMinimalViewInfo를 업데이트 해준다:
	 // - CameraComponent의 변화 사항을 반영해서 최종 렌더링까지 반영하게 됨
	DesiredView.Location = CameraModeView.Location;
	DesiredView.Rotation = CameraModeView.Rotation;
	DesiredView.FOV = CameraModeView.FieldOfView;
	DesiredView.OrthoWidth = OrthoWidth;
	DesiredView.OrthoNearClipPlane = OrthoNearClipPlane;
	DesiredView.OrthoFarClipPlane = OrthoFarClipPlane;
	DesiredView.AspectRatio = AspectRatio;
	DesiredView.bConstrainAspectRatio = bConstrainAspectRatio;
	DesiredView.bUseFieldOfViewForLOD = bUseFieldOfViewForLOD;
	DesiredView.ProjectionMode = ProjectionMode;
	DesiredView.PostProcessBlendWeight = PostProcessBlendWeight;
	if (PostProcessBlendWeight > 0.0f)
	{
		DesiredView.PostProcessSettings = PostProcessSettings;
	}
}

 

먼저 UpdateCameraModes 함수를 통해 위에서 언급했던 카메라 모드의 스택에서의 추가 및 제거가 일어나고, 카메라 모드가 변환될 때를 위한 블렌딩값이 계산된다.

 

void UZSCameraModeStack::EvaluateStack(float DeltaTime, FZSCameraModeView& OutCameraModeView)
{
	// Top -> Bottom [0 -> Num]까지 순차적으로 Stack에 있는 CameraMode 업데이트
	UpdateStack(DeltaTime);

	// Bottom -> Top까지 CameraModeStack에 대해 Blending 진행
	BlendStack(OutCameraModeView);
}

 

그리고 EvaluateStack함수에서 CameraModeStack에 있는 CameraMode를 업데이트(+블랜딩)하고 이에 대한 결과는 CameraModeView에 캐싱한다.

 

CameraModeView의 ControlRotation을 PlayerController의 ControlRotation에 반영해 주고,

World의 Rotation과 Location 값에도 반영해주고 카메라의 이동과 회전 또한 반영해준다.

 

마지막으로 나머지 렌더링에 필요한 값들을 세팅해주면 길고 길었던 카메라 세팅이 끝나게 된다!

 

 

CameraMode의 사용

 

다양한 종류의 카메라중 ThirdPerson(3인칭) 카메라를 만들어보자.

 

새로운 카메라를 만들 때 UpdateView함수를 override해서 필요한 설정 값을 업데이트 해주면된다.

void UZSCameraMode_ThirdPerson::UpdateView(float DeltaTime)
{
	FVector PivotLocation = GetPivotLocation();
	FRotator PivotRotation = GetPivotRotation();

	PivotRotation.Pitch = FMath::ClampAngle(PivotRotation.Pitch, ViewPitchMin, ViewPitchMax);

	View.Location = PivotLocation;
	View.Rotation = PivotRotation;
	View.ControlRotation = View.Rotation;
	View.FieldOfView = FieldOfView;

	// TargetOffsetCurve가 오버라이드되어 있다면, Curve에 값을 가져와서 적용 진행
	// - Camera 관점에서 Charater의 어느 부분에 Target으로 할지 결정하는 것으로 이해하면 됨
	if (TargetOffsetCurve)
	{
		const FVector TargetOffset = TargetOffsetCurve->GetVectorValue(PivotRotation.Pitch);
		View.Location = PivotLocation + PivotRotation.RotateVector(TargetOffset);
	}
}

 

ex) 카메라에 따라 위치나 각도 세팅을 UpdateView를 override해서 각 카메라모드마다 설정해주면 된다.

 

카메라모드를 ThirdPerson으로 설정

 

TargetOffsetCurve 값을 Curve Vector 그래프로 설정해주면 카메라가 캐릭터의 움직임이나 시야각(Pitch)에 따라 자연스럽게 이동하도록 보정할 수 있다.

→ 캐릭터가 위를 보면 카메라가 살짝 뒤로 이동하거나, 아래를 보면 더 가까워지는 식의 부드러운 움직임.

Curve Vector 그래프

 

 

이로서 카메라 세팅을 마친다.