Server-side Authoritative AI NPC 위치 동기화와 래그 보정 구현 가이드

서버 권위 구조에서 AI NPC의 위치가 순간이동하거나 뒤늦게 보이는 원인을 분석하고 스냅샷 보간·외삽 제한·클라이언트 재조정으로 안정적인 멀티플레이 동기화를 구현하는 방법을 정리합니다.

✓ 복사 완료!
챕터넷코드 & 상태 동기화 (Client Prediction/Lag)
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
Server-side Authoritative AI NPC 위치 동기화와 래그 보정 구현 가이드

핵심 요약

Server-side Authoritative 구조에서 AI NPC의 위치는 서버가 유일한 정답이어야 한다. 서버는 고정 Tick에서 NPC AI와 이동을 계산해 스냅샷을 전송하고 클라이언트는 약간 과거의 스냅샷을 보간해 표시한다. 패킷 손실 구간은 짧게 외삽하되 시간 제한을 두고 새 서버 상태가 도착하면 오차를 부드럽게 제거해야 순간이동과 역행을 줄일 수 있다.

AI NPC 위치 동기화에서 래그 보정은 왜 필요한가?

플레이어는 자신의 입력을 즉시 예측할 수 있지만 AI NPC는 서버가 행동 결정과 이동 결과를 모두 계산한다. 따라서 클라이언트가 받은 NPC 위치는 항상 네트워크 지연만큼 과거의 상태다. 받은 좌표를 즉시 화면에 적용하면 패킷 도착 간격의 흔들림, 즉 지터가 그대로 NPC 움직임에 드러난다.

대표적인 증상은 다음과 같다.

증상직접 원인권장 대응
NPC가 끊겨 보임최신 패킷 위치를 즉시 렌더링스냅샷 보간
NPC가 앞으로 튐패킷 지연 뒤 새 위치를 즉시 적용보간 버퍼와 오차 감쇠
NPC가 벽을 통과함긴 외삽 또는 클라이언트 판정외삽 시간 제한, 서버 충돌 권위
NPC가 되돌아감순서가 뒤바뀐 패킷 적용서버 Tick 또는 sequence 번호 검증

서버 Tick과 클라이언트 보간 버퍼를 설명하는 AI NPC 위치 동기화 개념도

Server-side Authoritative 구조에서 누가 무엇을 결정해야 하는가?

서버는 NPC의 목표 선택, 경로 탐색, 이동 속도, 충돌, 공격 판정을 결정한다. 클라이언트는 서버 스냅샷을 시각적으로 재생할 뿐이며 NPC 좌표를 서버에 제출하지 않는다. 이 경계는 치트 방지뿐 아니라 NPC마다 서로 다른 클라이언트 상태가 생기는 문제를 막는다.

flowchart LR
  AI[서버 AI와 경로 탐색] --> Sim[고정 Tick 이동·충돌 시뮬레이션]
  Sim --> Snap[Tick 번호 포함 상태 스냅샷]
  Snap --> Net[UDP 또는 신뢰성 계층]
  Net --> Buffer[클라이언트 스냅샷 버퍼]
  Buffer --> Render[과거 시점 보간 렌더링]

권장 스냅샷 필드

NPC별 전체 상태를 매 Tick 신뢰성 전송할 필요는 없다. 일반적인 위치 스냅샷에는 다음 정보가 있으면 충분하다.

public struct NpcSnapshot
{
    public uint ServerTick;
    public uint NpcId;
    public Vector3 Position;
    public Vector3 Velocity;
    public ushort Yaw;
    public byte MovementState; // Idle, Walk, Run, Knockback 등
}

ServerTick은 지연 시간 측정보다 중요하다. 클라이언트는 수신 순서가 아니라 Tick 순서로 스냅샷을 정렬하고 오래된 Tick은 버려야 한다. Velocity는 보간 품질을 높이고 짧은 패킷 손실에 대비한 제한적 외삽에 사용한다.

AI NPC 위치 보간은 어떻게 구현할까?

가장 단순하고 안정적인 방법은 현재 시각 대신 일정 시간 과거를 렌더링하는 것이다. 예를 들어 서버가 20 Hz로 스냅샷을 보내고 보간 지연을 100 ms로 잡으면 클라이언트는 버퍼에 쌓인 두 상태 사이를 자연스럽게 재생한다.

Step 1. 서버 이동은 고정 Tick으로 계산한다

서버의 시뮬레이션 간격을 dt로 고정한다. 프레임 시간에 따라 이동량이 바뀌지 않도록 NPC 위치는 다음처럼 갱신한다.

pt+1=pt+vt⋅dtp_{t+1} = p_t + v_t \cdot dt

경로 추종과 충돌 해결 후의 최종 위치만 스냅샷에 넣는다. 클라이언트가 별도 경로 탐색 결과로 NPC 위치를 확정하면 서버 결과와 누적 오차가 생긴다.

Step 2. 클라이언트에 Tick 순서 스냅샷 버퍼를 둔다

새 스냅샷의 Tick이 마지막으로 적용한 Tick보다 작거나 같으면 폐기한다. 남은 스냅샷은 ServerTick 기준으로 정렬된 작은 버퍼에 저장한다.

렌더링 대상 시점은 다음과 같다.

Trender=Tlatest−DinterpolationT_{render} = T_{latest} - D_{interpolation}

여기서 D_interpolation은 보간 지연이다. 처음에는 100 ms 정도로 시작하고 실제 지터와 전송 주기에 맞춰 조정한다. 지연이 너무 작으면 버퍼가 자주 비고 너무 크면 NPC 반응이 느려 보인다.

Step 3. 두 스냅샷 사이를 선형 보간한다

T_render를 감싸는 두 스냅샷 A, B를 찾는다. 보간 비율 α는 다음과 같다.

α=Trender−TATB−TA\alpha = \frac{T_{render} - T_A}{T_B - T_A} prender=(1−α)pA+αpBp_{render} = (1 - \alpha)p_A + \alpha p_B
Vector3 Interpolate(NpcSnapshot a, NpcSnapshot b, uint renderTick)
{
    float t = Mathf.InverseLerp(a.ServerTick, b.ServerTick, renderTick);
    return Vector3.LerpUnclamped(a.Position, b.Position, t);
}

회전은 위치처럼 선형 보간하지 말고 엔진의 구면 선형 보간을 사용한다. Unity라면 Quaternion.Slerp, Unreal Engine이라면 FQuat::Slerp 계열을 사용하면 된다.

패킷이 늦거나 유실되면 외삽은 어디까지 허용해야 할까?

보간할 다음 스냅샷이 없을 때만 마지막 위치와 속도로 짧게 예측한다.

ppredicted=plast+vlast⋅Δtp_{predicted} = p_{last} + v_{last} \cdot \Delta t

핵심은 상한이다. 보통 한두 번의 스냅샷 간격을 넘겨 외삽하지 않는다. 예를 들어 20 Hz 전송이라면 maxExtrapolation을 100 ms 전후로 제한하고 그 이후에는 마지막 위치를 유지하거나 이동 애니메이션만 멈춘다. 오래 외삽하면 NPC가 서버에서는 멈췄는데 클라이언트에서 계속 달리는 오류가 더 커진다.

상황클라이언트 처리
두 스냅샷이 존재위치·회전 보간
다음 스냅샷이 잠시 없음속도 기반 짧은 외삽
외삽 한도 초과마지막 서버 위치 유지
큰 위치 오차 또는 텔레포트 플래그즉시 스냅

텔레포트, 넉백, 워프처럼 연속 보간하면 안 되는 상태는 MovementState 또는 별도 Teleport 플래그를 스냅샷에 넣어 명시적으로 처리한다.

서버 스냅샷 도착 후 위치 오차는 어떻게 줄일까?

외삽 중이던 NPC에 새 서버 상태가 오면 렌더링 위치와 서버 위치 사이에 오차가 생길 수 있다. 작은 오차는 즉시 덮어쓰지 말고 짧은 시간 동안 감쇠한다. 단, 큰 오차는 부드럽게 고치려다 벽 밖이나 잘못된 공격 범위가 보일 수 있으므로 즉시 교정한다.

const float snapDistance = 2.0f;
const float correctionSeconds = 0.10f;

void ApplyServerCorrection(Vector3 authoritativePosition)
{
    float error = Vector3.Distance(renderPosition, authoritativePosition);

    if (error >= snapDistance)
    {
        renderPosition = authoritativePosition;
        correctionOffset = Vector3.zero;
        return;
    }

    correctionOffset += renderPosition - authoritativePosition;
}

void UpdateCorrection(float deltaTime)
{
    correctionOffset = Vector3.Lerp(
        correctionOffset,
        Vector3.zero,
        deltaTime / correctionSeconds
    );
}

이 코드는 표시 위치만 보정한다. 충돌, 피격, 어그로, 스킬 범위 판정은 항상 서버 좌표로 처리해야 한다.

AI NPC 래그 보정 구현 체크리스트

  1. 서버의 고정 Tick에서 AI, 이동, 충돌을 순서대로 계산한다.
  2. 모든 NPC 스냅샷에 단조 증가하는 ServerTick을 넣고 클라이언트에서 오래된 패킷을 버린다.
  3. 클라이언트는 최신 상태가 아니라 D_interpolation만큼 과거 상태를 보간해 렌더링한다.
  4. 버퍼가 비었을 때만 Velocity로 짧게 외삽하고 최대 시간을 제한한다.
  5. 작은 오차는 50~150 ms에 걸쳐 감쇠하고 워프나 큰 오차는 즉시 스냅한다.
  6. NPC의 공격과 충돌 판정은 클라이언트 보간 위치를 신뢰하지 않는다.

자주 묻는 질문 (FAQ)

NPC에도 클라이언트 예측이 필요한가?

대부분의 일반 NPC에는 필요 없다. 서버 결과를 보간하는 편이 구현과 검증이 단순하다. 플레이어가 직접 조작하는 소환수처럼 즉각적인 반응이 핵심인 경우에만 제한적으로 예측을 검토한다.

TCP를 쓰면 위치 동기화 문제가 해결되는가?

아니다. TCP는 순서와 전달을 보장하지만 앞선 패킷이 지연되면 뒤 패킷도 기다리는 head-of-line blocking이 생길 수 있다. 실시간 위치는 최신성이 중요하므로 전송 방식과 무관하게 Tick 기반 보간 및 오래된 상태 폐기가 필요하다.

NPC마다 매 Tick 전체 상태를 보내야 하는가?

아니다. 거리 기반 관심 영역(Interest Management)으로 관찰 중인 NPC만 보내고 위치는 비신뢰성 스냅샷으로 갱신한다. 생성·파괴·중요 상태 전환처럼 누락되면 안 되는 이벤트만 별도 신뢰성 채널을 고려한다.

정리

AI NPC 동기화의 목표는 클라이언트가 NPC를 계산하게 하는 것이 아니라 서버가 확정한 과거 상태를 끊김 없이 보여 주는 것이다. 고정 Tick 서버 시뮬레이션, Tick 순서 스냅샷, 보간 버퍼, 제한된 외삽, 임계값 기반 재조정을 함께 적용하면 지연과 패킷 손실 속에서도 서버 권위를 유지할 수 있다.