레인보우 식스 시즈, 타르코프 등 많은 FPS 장르의 게임들이 그렇듯, 마우스를 움직인다고 캐릭터의 하반신까지 전부 돌아가진 않는다.
만약 하반신까지 돌아갈 경우, 발이 목표 회전까지 미끄러지듯 움직이기 때문에 부자연스럽다.
그래서 언리얼은 상반신만 움직일 수 있도록 AimOffset을 마련해뒀다.
준비물은 1프레임짜리 애니메이션 에셋이다.

이런 1프레임짜리 애니메이션 에셋을 9개 준비한다.
가운데 정면, 가운데 위, 가운데 아래, 왼쪽 정면, 왼쪽 위...
이렇게 총 9개 준비하면 된다.

이걸 AimOffset 안에 넣어주면 자연스럽게 Blend된다.
비조준 시, 조준 시 모두 만들어준다.

가로, 세로 축 설정은 이렇다.
// BlasterCharacter.cpp
AO_Yaw = GetBaseAimRotation().Yaw
AO_Pitch = GetBaseAimRotation().Pitch;
캐릭터에게 float 변수를 선언하고, 자신이 바라보는 Yaw 방향과 Pitch 방향을 Tick으로 업데이트한다.
// BlasterAnimInstance.cpp
AO_Yaw = BlasterCharacter->GetAO_Yaw();
AO_Pitch = BlasterCharacter->GetAO_Pitch();
if (AO_Pitch > 90.f && !IsLocallyControlled())
{
// pitch의 270 ~ 360을 -90 ~ 0 으로 바꾸는 과정
FVector2D InRange(270.f, 360.f);
FVector2D OutRange(-90.f, 0.f);
AO_Pitch = FMath::GetMappedRangeValueClamped(InRange, OutRange, AO_Pitch);
}
Getter함수로 열어둔 두 변수를 AnimInstance로 가지고 온다. 단, pitch는 Replicate 되는 과정에서 음수로 넘어갈 경우 360으로 복제하기 때문에, 이를 수정하는 로직도 작성했다

만들어뒀던 Equipped 캐시포즈를 가져와 AO_Yaw와 AO_Pitch를 넣어주고, 마찬가지로 캐시 포즈로 만들어준다.

저번 FABRIK 포스팅에 있던 게 이거다.

잘 작동하는 모습이다.
하지만 아직 하반신이 돌아가는 모습을 만들어주지 않았다. 화면을 좌우로 90도 넘게 돌리면 다리를 움직여 보는 방향으로 움직이게 해줄 거다.
작동 원리는 이렇다.

// BlasterCharacter.cpp
void ABlasterCharacter::AimOffset(float DeltaTime)
{
...
if (Speed == 0.f && !bIsInAir) // 가만히 서있고 점프 안 할 때
{
FRotator CurrentAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f);
FRotator DeltaAimRotation = UKismetMathLibrary::NormalizedDeltaRotator(CurrentAimRotation, StartingAimRotation);
AO_Yaw = DeltaAimRotation.Yaw;
if (TurningInPlace == ETurningInPlace::ETIP_NotTurning)
{
InterpAO_Yaw = AO_Yaw;
}
bUseControllerRotationYaw = true;
TurnInPlace(DeltaTime);
}
if (Speed > 0.f || bIsInAir) // 움직이거나 점프 중일 때
{
StartingAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f);
AO_Yaw = 0.f;
bUseControllerRotationYaw = true;
TurningInPlace = ETurningInPlace::ETIP_NotTurning;
}
AO_Pitch = GetBaseAimRotation().Pitch;
if (AO_Pitch > 90.f && !IsLocallyControlled())
{
// pitch의 270 ~ 360을 -90 ~ 0 으로 바꾸는 과정
FVector2D InRange(270.f, 360.f);
FVector2D OutRange(-90.f, 0.f);
AO_Pitch = FMath::GetMappedRangeValueClamped(InRange, OutRange, AO_Pitch);
}
}
void ABlasterCharacter::TurnInPlace(float DeltaTime)
{
if (AO_Yaw > 90.f)
{
TurningInPlace = ETurningInPlace::ETIP_Right;
}
else if (AO_Yaw < -90.f)
{
TurningInPlace = ETurningInPlace::ETIP_Left;
}
if (TurningInPlace != ETurningInPlace::ETIP_NotTurning)
{
InterpAO_Yaw = FMath::FInterpTo(InterpAO_Yaw, 0.f, DeltaTime, 4.f);
AO_Yaw = InterpAO_Yaw;
if (FMath::Abs(AO_Yaw) < 15.f)
{
TurningInPlace = ETurningInPlace::ETIP_NotTurning;
StartingAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f);
}
}
}
움직이거나 점프할 땐 이런 회전이 필요없으므로 AO_Yaw와 StartingAimRotation을 0으로 한다.
가만히 서있는 경우 CurrentAimRotation을 선언, 캐릭터의 Yaw값을 담고
DeltaAimRotation을 선언, StartAimRotation과 CurrentAimRotation을 비교해 변화량을 담는다.
AO_Yaw에도 해당 Yaw값을 담아준다.
회전이 필요하지 않으면(아직 90도를 넘지 않았다면) InterpAO_Yaw에도 그대로 담아준다.
액터 방향이 곧 화면 방향이므로 bUseControllerRotationYaw를 true로 바꿔준다.
그 뒤 TurnInPlace로 진입
AO_Yaw의 절댓값이 90을 넘기면(StartingAimRotation과 CurrentAimRotation의 Yaw 값이 90 이상 차이가 나면)
회전이 필요하기 때문에 Enum의 값을 변경하고
InterpAO_Yaw를 0까지 점진적으로 변화시키며
해당 값을 AO_Yaw로 넣어준다
만약 AO_Yaw의 절댓값이 15이하인 경우(액터 방향과 루트본의 방향의 차이가 15 이하인 경우)
회전이 필요 없음을 알리고 StartingAimRotation을 현재 방향으로 초기화한다.

AnimBP에 AO_Yaw값을 이용한 루트 본 회전을 넣어준다.

결과는 이렇다.
다행히도 잘 작동하는 모습이다.