kwivaro웹 도구
UNREAL · UI · 게임 시스템

나침반 바늘이 ±180°에서 튀었다

게임 클라이언트 개발자2026-09-107분

HUD 나침반 띠가 카메라를 따라 잘 움직이다가, 남쪽/북쪽 경계를 넘는 순간 반대편으로 확 점프했다. 원인은 단순했다. 각도를 그대로 보간한 것. 각도는 원형(circular) 값이라 선형 보간이 경계에서 깨진다.

목표

카메라를 돌릴 때 나침반 띠가 순간이동하듯 튀지 않고 부드럽게 따라오게 한다. 특히 방위각이 ±180° 경계를 넘어갈 때 방위 표시가 한쪽으로 확 점프하지 않게 하는 게 핵심이다.

원인 — 각도를 직접 보간하면 ±180° 경계에서 값이 튄다

나침반은 카메라 Yaw를 기준으로 각 방위의 화면 X 위치를 잡는다. 회전을 부드럽게 하려고 Yaw를 보간했는데, Yaw는 +180°와 -180°가 같은 방향(남쪽)인 원형 값이다. 카메라가 남쪽을 지나 +179° → -179°로 넘어가는 순간 숫자로는 358°가 점프한다. FindDeltaAngleDegrees로 최단각을 구하는 방식도 이 경계에서는 실측상 큰 점프가 찍혔다.

여기에 별개 증상이 하나 더 겹쳐 있었다. Slate가 NativePaint 결과를 캐시로 들고 있어서, 카메라는 계속 도는데 화면은 이전 프레임에 멈춰 있다가 캐시가 갱신되는 순간 한꺼번에 확 바뀌는 것처럼 보였다.

해결 — Yaw를 2D 단위벡터로 바꿔 보간한 뒤 각도로 역산

각도에는 경계가 있지만 그 각도가 가리키는 방향 벡터에는 경계가 없다. 그래서 Yaw를 (cos, sin)로 바꿔 벡터를 보간하고, 다시 정규화한 뒤 Atan2로 각도를 되돌리는 방식으로 갈아엎었다.

const float RawYawRad = FMath::DegreesToRadians(RawYaw);
const FVector2D RawDir(FMath::Cos(RawYawRad), FMath::Sin(RawYawRad));

// 각도 직접 보간은 ±180 경계에서 점프. 2D 단위벡터를 보간 후 재정규화하면
// "각도 경계"라는 개념 자체가 없어 항상 연속적으로 움직인다.
const float LerpAlpha = FMath::Clamp(DeltaTime * CompassYawInterpSpeed, 0.f, 1.f);
const FVector2D LerpedDir = FMath::Lerp(SmoothedYawDir, RawDir, LerpAlpha);
SmoothedYawDir = LerpedDir.IsNearlyZero() ? RawDir : LerpedDir.GetSafeNormal();
SmoothedCameraYaw = FMath::RadiansToDegrees(FMath::Atan2(SmoothedYawDir.Y, SmoothedYawDir.X));

그리기와 마커 갱신은 이 보간값(SmoothedCameraYaw)만 읽게 했다. 그리고 캐싱 때문에 늦게 튀는 건 NativeTick에서 매 프레임 리페인트를 강제해서 잡았다.

void URetrieveCompassWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
    Super::NativeTick(MyGeometry, InDeltaTime);
    UpdateSmoothedCameraYaw(GetOwningPlayer(), InDeltaTime);
    UpdateWidgetMarkers(MyGeometry);
    Invalidate(EInvalidateWidgetReason::Paint); // 캐시로 인한 지연/점프 방지
}

방위 위치 계산은 각 방위와 카메라 Yaw의 상대각을 [-180, 180]로 정규화한 뒤 시야각 밖이면 걸러낸다. 여기에 NativePaint 시작에서 PushClip(FSlateClippingZone(AllottedGeometry))을 더해, 회전 중 방위 텍스트가 띠 밖으로 새어 나가는 것도 같이 막았다.

배운 점

각도는 원형 위상이라 선형 보간이 경계에서 깨진다. 값이 아니라 그 값이 가리키는 벡터를 보간하면 경계 케이스가 아예 사라진다. ±180°에서 튀는 문제를 만나면 최단각을 구하는 것보다 방향 벡터로 바꿔 보간 후 Atan2로 되돌리는 게 근본적이다.

Slate의 NativePaint는 캐싱된다. 값은 매 틱 갱신되는데 화면만 뒤늦게 확 바뀐다면 Invalidate(EInvalidateWidgetReason::Paint)로 리페인트를 강제하는 걸 의심한다.

위치 계산에서 범위 밖을 걸러도 그리기 자체의 클리핑은 별개다. 좌표를 걸러내도, 실제로 그려지는 요소가 위젯 경계를 안 넘게 하려면 PushClip으로 클리핑 영역을 따로 잡아야 한다.

방위·회전처럼 ±180°를 넘나드는 값이면 나침반이든 미니맵이든 레이더든 벡터 보간이 똑같이 안전하다.