Client Prediction과 Lag Compensation으로 멀티플레이 이동 지연 보정하기

멀티플레이 게임에서 입력 지연, 위치 되돌림, 판정 불일치를 줄이는 Client Prediction·Server Reconciliation·Lag Compensation의 원리와 Unity C# 구현 절차를 정리합니다.

✓ 복사 완료!
챕터일반 가이드
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
Client Prediction과 Lag Compensation으로 멀티플레이 이동 지연 보정하기

게임 네트워크 이동 보정의 핵심은 클라이언트가 자신의 입력을 즉시 예측해 보여 주고 서버는 권위 있는 상태를 확정한 뒤 차이를 다시 조정하는 것이다. 사격처럼 과거 시점의 판정이 필요한 행동에는 서버가 대상의 과거 상태를 조회하는 Lag Compensation을 적용한다. 이 조합은 체감 입력 지연과 고지연 환경의 불공정한 판정을 함께 줄인다.

네트워크 이동에서 지연과 위치 되돌림은 왜 발생하는가?

클라이언트가 이동 입력을 서버에 보내고 서버가 시뮬레이션한 위치를 다시 받는 왕복 시간은 RTT(Round-Trip Time)다. RTT가 100ms라면 입력 결과를 서버 응답까지 기다리는 방식은 매 이동마다 약 100ms의 지연을 만든다.

서버만 이동을 확정하는 구조는 치트 방지와 동기화에는 유리하지만 조작감이 나빠진다. 반대로 클라이언트 위치를 그대로 신뢰하면 속도 핵, 텔레포트, 벽 통과를 막기 어렵다. 따라서 서버 권위(authoritative server)는 유지하고 화면 반응만 클라이언트에서 앞당겨야 한다.

한 틱의 이동은 보통 다음처럼 계산한다.

pt+1=pt+v(inputt)×Δtp_{t+1}=p_t+v(input_t)\times\Delta t

여기서 서버와 클라이언트는 같은 tickRate, 이동 속도, 충돌 규칙을 사용해야 한다. 하나라도 다르면 예측 오차가 누적된다.

Client Prediction은 어떻게 동작하는가?

Client Prediction은 로컬 플레이어의 입력을 서버 응답 전에 즉시 시뮬레이션하는 방식이다. 클라이언트는 입력마다 증가하는 시퀀스 번호를 붙여 저장하고 서버에도 같은 입력을 전송한다.

flowchart LR
  A[클라이언트 입력 tick 120] --> B[로컬 즉시 시뮬레이션]
  A --> C[서버로 입력 전송]
  C --> D[서버 권위 시뮬레이션]
  D --> E[확정 상태 + 마지막 처리 입력 번호]
  E --> F[클라이언트 보정 및 미확정 입력 재실행]

클라이언트는 서버가 처리한 마지막 입력 번호보다 큰 입력만 미확정 입력으로 보관한다. 서버 응답이 오면 해당 입력 번호까지는 확정된 것으로 제거한다.

역할클라이언트서버
입력 반영즉시 예측수신 순서에 따라 처리
최종 위치임시 화면 상태권위 있는 게임 상태
충돌 검증체감용 또는 동일 규칙반드시 검증
치트 대응신뢰하지 않음속도, 이동량, 충돌 검증

Step 1. 입력에 tick과 sequence를 붙인다

tick은 시뮬레이션 시간 단위이고 sequence는 입력 순서를 식별한다. 재전송이나 패킷 지연이 있어도 서버가 마지막 처리 입력을 클라이언트에 알려 줄 수 있다.

public readonly struct MoveInput
{
    public uint Sequence { get; }
    public float Horizontal { get; }
    public float Vertical { get; }
    public float DeltaTime { get; }

    public MoveInput(uint sequence, float horizontal, float vertical, float deltaTime)
    {
        Sequence = sequence;
        Horizontal = horizontal;
        Vertical = vertical;
        DeltaTime = deltaTime;
    }
}

DeltaTime을 클라이언트가 임의로 보내게 두면 조작될 수 있다. 실서비스에서는 서버의 고정 틱 간격을 사용하거나 입력에 tick만 보내고 서버가 1f / tickRate를 적용하는 편이 안전하다.

Step 2. 같은 이동 함수를 클라이언트와 서버에서 사용한다

예측과 서버 처리의 규칙이 달라지면 매 응답마다 보정이 발생한다. 이동 계산은 가능한 한 순수 함수로 분리한다.

public static Vector3 SimulateMove(Vector3 position, MoveInput input, float speed)
{
    var direction = new Vector3(input.Horizontal, 0f, input.Vertical);
    direction = Vector3.ClampMagnitude(direction, 1f);
    return position + direction * speed * input.DeltaTime;
}

중력, 경사면, CharacterController, 물리 엔진 충돌처럼 플랫폼별 오차가 큰 요소는 완전한 결정론을 기대하기 어렵다. 이 경우에도 서버 결과를 기준으로 재조정하면 되지만 보정 빈도를 줄이려면 로컬과 서버의 충돌 레이어 및 이동 파라미터를 맞춰야 한다.

Step 3. Server Reconciliation으로 예측 오차를 되감고 재실행한다

서버 응답에는 권위 위치와 마지막 처리 입력 번호를 포함한다. 클라이언트는 권위 위치로 되돌아간 뒤 아직 서버가 처리하지 않은 입력을 순서대로 다시 적용한다.

public Vector3 Reconcile(
    Vector3 serverPosition,
    uint lastProcessedSequence,
    List<MoveInput> pendingInputs,
    float speed)
{
    pendingInputs.RemoveAll(input => input.Sequence <= lastProcessedSequence);

    var corrected = serverPosition;
    foreach (var input in pendingInputs)
        corrected = SimulateMove(corrected, input, speed);

    return corrected;
}

이 함수가 반환한 값은 논리 위치에 즉시 적용한다. 화면상의 흔들림이 문제라면 렌더링 위치만 짧게 보간한다. 논리 위치까지 보간하면 충돌과 다음 예측의 기준이 흔들릴 수 있다.

보정 크기는 다음처럼 확인할 수 있다.

error=∥ppredicted−pserver∥error=\left\|p_{predicted}-p_{server}\right\|

error가 작은 경우에는 렌더링 오프셋을 수십 밀리초 동안 감쇠시키고 큰 경우에는 즉시 스냅하는 임계값을 둔다. 다만 임계값은 시각 처리일 뿐 서버 상태를 무시하는 장치가 아니다.

Lag Compensation은 사격 판정을 어떻게 공정하게 만드는가?

Client Prediction은 내 이동 반응을 빠르게 하지만 서버가 현재 시점만 보고 총알 명중을 판정하면 고지연 플레이어는 불리하다. 플레이어가 조준한 화면은 과거의 원격 플레이어 상태이기 때문이다.

Lag Compensation은 서버가 각 플레이어의 최근 히트박스 상태를 틱별로 보관했다가, 발사 입력이 발생한 과거 시점의 상태로 판정하는 기법이다. 흔히 서버 리와인드(server rewind)라고도 부른다.

서버 리와인드 판정 절차

  1. 서버는 매 틱 플레이어의 위치와 히트박스 상태를 히스토리 버퍼에 저장한다.
  2. 발사 요청의 클라이언트 틱 또는 서버가 추정한 발사 시각을 받아 허용 범위로 제한한다.
  3. 해당 시점의 대상 히트박스를 조회해 레이캐스트 또는 투사체 판정을 수행한다.
  4. 판정이 끝나면 현재 월드 상태를 유지한 채 피해 결과만 적용한다.
항목권장 원칙
히스토리 길이최대 보정 시간보다 조금 길게 유지. 예: 20Hz에서 1초면 20개 스냅샷
요청 시각현재 서버 시각 기준 허용 범위로 clamp
판정 대상플레이어 히트박스 등 필요한 상태만 저장
월드 변경실제 Transform을 되감기보다 스냅샷 기반 교차 판정 선호
보안클라이언트가 보낸 명중 대상과 피해량은 신뢰하지 않음

예를 들어 20Hz 서버에서 최근 1초만 보정한다면 너무 오래된 발사 요청은 가장 오래된 스냅샷으로 제한하거나 거부해야 한다. 무제한 리와인드는 고지연 사용자에게 과도한 이점을 주고 메모리 사용량도 늘린다.

public Snapshot? FindSnapshotAt(
    IReadOnlyList<Snapshot> history,
    uint requestedTick,
    uint oldestAllowedTick,
    uint currentServerTick)
{
    if (requestedTick > currentServerTick)
        return null;

    var tick = Math.Max(requestedTick, oldestAllowedTick);
    return history
        .Where(snapshot => snapshot.Tick <= tick)
        .OrderByDescending(snapshot => snapshot.Tick)
        .Select(snapshot => (Snapshot?)snapshot)
        .FirstOrDefault();
}

실제 구현에서는 매 요청마다 정렬하지 말고 고정 크기 링 버퍼와 틱 인덱스를 사용해 조회 비용을 일정하게 유지하는 편이 낫다. 히트박스가 빠르게 움직이는 게임은 인접한 두 스냅샷 사이를 보간해 판정 오차를 줄일 수 있다.

Client Prediction과 Lag Compensation의 적용 범위는 무엇인가?

두 기법은 대체 관계가 아니다. Client Prediction은 로컬 조작감, Lag Compensation은 과거 화면 기준의 서버 판정에 초점이 있다.

상황우선 적용 기법이유
내 캐릭터 이동Client Prediction + Reconciliation즉각적인 입력 반응 필요
다른 플레이어 표시Snapshot Interpolation패킷 도착 간격의 흔들림 완화
히트스캔 무기Lag Compensation조준 시점의 대상 상태 판정
투사체 무기서버 투사체 시뮬레이션경로와 충돌을 서버가 권위 있게 계산
아이템 획득서버 검증중복 획득과 거리 조작 방지

원격 플레이어까지 예측하면 잘못된 행동을 보여 줄 가능성이 커진다. 원격 엔티티는 보통 약간 과거 시점의 스냅샷을 보간하는 방식이 더 안정적이다.

자주 묻는 질문 (FAQ)

RTT가 낮아도 Client Prediction이 필요한가?

필요하다. 30~50ms RTT도 이동 시작과 점프 반응에서 느껴질 수 있다. 다만 보정 오차가 작으므로 구현과 디버깅이 훨씬 단순해진다.

Lag Compensation을 이동에도 적용해야 하는가?

아니다. 이동은 서버 권위 시뮬레이션과 Client Prediction으로 처리한다. Lag Compensation은 보통 발사, 근접 공격처럼 과거 시점의 충돌 판정이 필요한 요청에 사용한다.

클라이언트 tick을 그대로 믿어도 되는가?

안 된다. 서버는 요청 tick을 현재 서버 tick 기준의 최대 과거 범위로 제한하고 미래 tick도 거부하거나 현재 tick으로 보정해야 한다.

정리

서버는 이동과 피해의 최종 권위를 유지하고 클라이언트는 자신의 입력만 즉시 예측한다. 서버 응답마다 마지막 처리 입력을 기준으로 재실행하면 위치 되돌림을 최소화할 수 있다. 사격 판정에는 제한된 히스토리 버퍼 기반의 Lag Compensation을 추가해 고지연 환경에서도 일관된 판정을 제공하자.