개요

일반 상태와 조준 상태를 구분하여 TPS뷰-숄더뷰를 전환한다.

1) 소켓 오프셋 vs 타깃 오프셋

스프링암 SocketOffset & TargetOffset

스프링암과 카메라 컴포넌트를 캐릭터에 추가한 이후 캐릭터 BP를 열어보면 원점을 촬영하고 있는걸 볼 수 있다. 이대로는 캐릭터가 폭탄을 어디로 던지는지 확인하기 어렵기 때문에, 우측 상단으로 위치를 조정했다.

Untitled

위 이미지처럼 스프링암 자체의 트랜스폼을 옮기면 카메라와 캐릭터 사이의 물체 탐지, 위치 이상 등의 문제가 발생할 수 있다. 카메라 항목의 소켓 오프셋과 타깃 오프셋을 조정하는 것이 좋다.

Untitled

둘 모두 카메라의 위치를 조정하지만 동작에 있어 차이가 크다. 뷰포트에서는 동일하게 움직이기에 차이를 확인할 수 없다.

Untitled

위 이미지는 타깃 오프셋의 Z값을 동일하게 유지한 상태에서 각 오프셋의 Y값만을 지정했을 때의 모습이다. 타깃 오프셋을 지정했을 때는 캐릭터와의 거리가 일정하지 않고, 소켓 오프셋을 지정했을 때는 거리가 일정한 모습이다. 사실 TargetArmLength는 둘 모두 100으로 동일하다. 왜 차이가 발생할까? 그건 회전 지점이 다르기 때문이다.

이를 확인하기 위해 테스트를 진행했다. 각 오프셋을 지정한 상태에서 pitch를 고정, 마우스롤 돌려보았다.

  • 빨간색 - 카메라 위치
  • 파란색 - 소켓 오프셋 위치
  • 초록색 - 타깃 오프셋 위치
  • 보라색 - 카메라 시작 위치

소켓 오프셋

Untitled

Untitled

소켓 오프셋은 스프링 암의 끝인 카메라의 위치를 조정한다. 회전 중심에는 영향을 미치지 않기 때문에, 위 이미지처럼 회전한다면 캐릭터를 중심으로 원이 형성된다.

타깃 오프셋

Untitled

Untitled

타깃 오프셋은 스프링 암의 뿌리, 대상에 부착되는 위치를 조정한다. 회전 중심에 영향을 미치는 요소이다. 따라서 위 이미지처럼 회전한다면 타깃 오프셋의 위치를 중심으로 원이 형성된다.

왜 그럴까

// SpringArmComponent.cpp
FRotator DesiredRot = GetTargetRotation();

// Get the spring arm 'origin', the target we want to look at
FVector ArmOrigin = GetComponentLocation() + TargetOffset;
// We lag the target, not the actual camera position, so rotating the camera around does not have lag
FVector DesiredLoc = ArmOrigin;

// Now offset camera position back along our rotation
DesiredLoc -= DesiredRot.Vector() * TargetArmLength;
// Add socket offset in local space
DesiredLoc += FRotationMatrix(DesiredRot).TransformVector(SocketOffset);

스프링암 컴포넌트 코드를 보면 카메라의 최종 위치를 구하는 과정에서 타깃 오프셋→TargetArmLength→소켓 오프셋 순서대로 적용되는 것을 볼 수 있다. 타깃 오프셋은 컴포넌트 위치에 더해져 회전 원점인 ArmOrigin을 형성한다. 소켓 오프셋은 마지막에 추가적으로 위치를 조정하는 형태이다.

2) 타임라인

카메라의 위치를 부드럽게 조정하기

나는 두 대의 카메라를 사용하지 않고 한 대의 카메라에서 TargetArmLength만 변경하는 방식을 사용했다. 값의 변화를 연속적으로 만들기 위하여 선형보간(Lerp함수 사용)을 사용했고, 선형보간에 필요한 시간과 알파 값은 타임라인을 활용해 해결했다.

타임라인 사용법

  1. 헤더에서 타임라인 관련 선언

     // CharacterPlayer.h
     #include "Components/TimelineComponent.h"
        
     protected:
     	UFUNCTION()
     	void CameraZoom(float Alpha);
        
     protected:
     	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Camera")
     	TObjectPtr<class UTimelineComponent> CameraTimelineComponent;
        
     	UPROPERTY(EditAnyWhere, Category = "Camera")
     	TObjectPtr<class UCurveFloat> CameraZoomCurve;
        
     	FOnTimelineFloat CameraZoomHandler;
     	float DefaultSpringArmLength;
     	float ZoomedSpringArmLength;
    
    • 타임라인 기능을 사용하기 위하여 UTimelineComponent가 필요하다.
    • 타임라인에 적용할 커브 또한 선언한다. 커브의 생성과 편집은 에디터에서 이루어진다. 커브에서 재생 시간(X값)과 해당 시간에서의 알파값(Y값)이 결정된다.
    • 타임라인 트랙을 관리하기 위한 함수 시그니처 또한 선언했다.
    • 선형 보간을 위한 두 TargetArmLength 또한 선언했다.
  2. 생성자

     // CharacterPlayer.cpp
     // 타임라인 컴포넌트를 서브 오브젝트로 생성
     CameraTimelineComponent = CreateDefaultSubobject<UTimelineComponent>(TEXT("CameraTimelineComponent"));
        
     // 에셋 로드 - 카메라
     static ConstructorHelpers::FObjectFinder<UCurveFloat> CameraZoomCurveFinder
     (TEXT("/Game/Character/Curves/CT_CameraZoom.CT_CameraZoom"));
     if (CameraZoomCurveFinder.Succeeded())
     {
     	CameraZoomCurve = CameraZoomCurveFinder.Object;
     }
    
    • 타임라인 컴포넌트를 서브 오브젝트로 생성한다.
    • 커브는 BP로 만들어 뒀다. 이를 로드하여 사용.
  3. BeginPlay

     	// 카메라 줌 설정
     	CameraZoomHandler.BindUFunction(this, FName("CameraZoom"));
     	CameraTimelineComponent->AddInterpFloat(CameraZoomCurve, CameraZoomHandler);
    
    • 핸들러를 사용해 대리자에 함수를 등록한다.
    • 타임라인 컴포넌트에서 제공하는 AddInterpFloat를 사용해 커브와 핸들러를 묶는다. 해당 핸들러는 이 커브를 사용한다는 의미.
  4. 실제 사용

     // 조준 시작
     CameraTimelineComponent->Play();
     // 조준 해제
     CameraTimelineComponent->Reverse();
    
    • 조준/조준해제에 맞추어 사용하면 된다. 타임라인의 시간값에 설정된 알파값이 Lerp에 사용되어 TargetArmLength를 부드럽게 조정한다.

커브 예시

Untitled

Untitled

1초동안 0→1로 값을 변경한다.

3) 최종 결과

Untitled

참고자료