부드러운 줌-아웃 기능 제작
Socket Offset, Target Offset, Timeline
개요
일반 상태와 조준 상태를 구분하여 TPS뷰-숄더뷰를 전환한다.
1) 소켓 오프셋 vs 타깃 오프셋
스프링암 SocketOffset & TargetOffset
스프링암과 카메라 컴포넌트를 캐릭터에 추가한 이후 캐릭터 BP를 열어보면 원점을 촬영하고 있는걸 볼 수 있다. 이대로는 캐릭터가 폭탄을 어디로 던지는지 확인하기 어렵기 때문에, 우측 상단으로 위치를 조정했다.
위 이미지처럼 스프링암 자체의 트랜스폼을 옮기면 카메라와 캐릭터 사이의 물체 탐지, 위치 이상 등의 문제가 발생할 수 있다. 카메라 항목의 소켓 오프셋과 타깃 오프셋을 조정하는 것이 좋다.
둘 모두 카메라의 위치를 조정하지만 동작에 있어 차이가 크다. 뷰포트에서는 동일하게 움직이기에 차이를 확인할 수 없다.
위 이미지는 타깃 오프셋의 Z값을 동일하게 유지한 상태에서 각 오프셋의 Y값만을 지정했을 때의 모습이다. 타깃 오프셋을 지정했을 때는 캐릭터와의 거리가 일정하지 않고, 소켓 오프셋을 지정했을 때는 거리가 일정한 모습이다. 사실 TargetArmLength는 둘 모두 100으로 동일하다. 왜 차이가 발생할까? 그건 회전 지점이 다르기 때문이다.
이를 확인하기 위해 테스트를 진행했다. 각 오프셋을 지정한 상태에서 pitch를 고정, 마우스롤 돌려보았다.
- 빨간색 - 카메라 위치
- 파란색 - 소켓 오프셋 위치
- 초록색 - 타깃 오프셋 위치
- 보라색 - 카메라 시작 위치
소켓 오프셋
소켓 오프셋은 스프링 암의 끝인 카메라의 위치를 조정한다. 회전 중심에는 영향을 미치지 않기 때문에, 위 이미지처럼 회전한다면 캐릭터를 중심으로 원이 형성된다.
타깃 오프셋
타깃 오프셋은 스프링 암의 뿌리, 대상에 부착되는 위치를 조정한다. 회전 중심에 영향을 미치는 요소이다. 따라서 위 이미지처럼 회전한다면 타깃 오프셋의 위치를 중심으로 원이 형성된다.
왜 그럴까
// 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함수 사용)을 사용했고, 선형보간에 필요한 시간과 알파 값은 타임라인을 활용해 해결했다.
타임라인 사용법
-
헤더에서 타임라인 관련 선언
// 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 또한 선언했다.
-
생성자
// 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로 만들어 뒀다. 이를 로드하여 사용.
-
BeginPlay
// 카메라 줌 설정 CameraZoomHandler.BindUFunction(this, FName("CameraZoom")); CameraTimelineComponent->AddInterpFloat(CameraZoomCurve, CameraZoomHandler);
- 핸들러를 사용해 대리자에 함수를 등록한다.
- 타임라인 컴포넌트에서 제공하는 AddInterpFloat를 사용해 커브와 핸들러를 묶는다. 해당 핸들러는 이 커브를 사용한다는 의미.
-
실제 사용
// 조준 시작 CameraTimelineComponent->Play(); // 조준 해제 CameraTimelineComponent->Reverse();
- 조준/조준해제에 맞추어 사용하면 된다. 타임라인의 시간값에 설정된 알파값이 Lerp에 사용되어 TargetArmLength를 부드럽게 조정한다.
커브 예시
1초동안 0→1로 값을 변경한다.
3) 최종 결과
참고자료
- 언리얼 타임라인 사용법 - https://dev.epicgames.com/documentation/en-us/unreal-engine/creating-timelines-in-unreal-engine?application_version=5.3
- 1인칭 3인칭 전환 - https://www.youtube.com/watch?v=CW59e1SEvWo&ab_channel=MattAspland
- Timeline 사용법 블로그-1 - https://sheep-adult.tistory.com/15
- Timeline 사용법 블로그-2 - https://lykanstudio.tistory.com/40
- TargetOffset, SocketOffset - https://www.bilibili.com/read/cv14318016/
- TargetOffset, SocketOffset - https://soramame-games.com/labyrinth-camera-control