Client Prediction과 Lag Compensation 적용하기

멀티플레이 액션 게임에서 조작 반응성을 유지하면서도 서버 권위를 지키는 Client Prediction과 Lag Compensation의 설계 원리와 구현 흐름을 정리합니다.

✓ 복사 완료!
챕터일반 가이드
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
Client Prediction과 Lag Compensation 적용하기

왜 두 기법이 필요한가

실시간 멀티플레이 게임에서 입력을 서버에 보내고 응답을 받은 뒤에만 플레이어를 움직이면 왕복 지연 시간만큼 조작이 늦게 반영된다. RTT가 80ms라면 버튼을 누른 뒤 결과를 보기까지 적어도 그 정도의 시간이 걸린다. 이동과 점프처럼 즉각적인 피드백이 중요한 동작에서는 이 지연이 크게 느껴진다.

반대로 클라이언트가 모든 결과를 결정하게 두면 치트에 취약하고 다른 플레이어와의 상태도 쉽게 어긋난다. 일반적인 해법은 서버를 최종 권위로 유지하되 내 조작은 클라이언트에서 먼저 예측하고 서버 응답이 오면 정정하는 방식이다.

클라이언트 예측과 서버 정정이 시간축에서 진행되는 모습

전체 흐름

Client Prediction은 로컬 플레이어의 이동을 즉시 시뮬레이션하는 기법이다. 서버는 같은 입력을 다시 시뮬레이션하여 권위 있는 결과를 만들고 클라이언트는 그 결과를 기준으로 필요할 때 자신을 되감아 재실행한다.

flowchart LR
    A[클라이언트 입력 생성] --> B[입력 번호 부여 후 서버 전송]
    A --> C[로컬에서 즉시 예측 시뮬레이션]
    B --> D[서버가 입력 검증 및 시뮬레이션]
    D --> E[권위 상태와 처리한 입력 번호 전송]
    E --> F[클라이언트 상태 비교]
    F -->|차이가 작음| G[현재 예측 유지]
    F -->|차이가 큼| H[권위 상태로 복원]
    H --> I[미처리 입력 재실행]
    I --> G

이 흐름에서 중요한 점은 클라이언트가 위치만 보내는 것이 아니라 입력을 보내야 한다는 것이다. 위치를 신뢰하면 순간이동이나 속도 조작을 검증하기 어렵다. 서버는 이동 입력, 점프 요청, 바라보는 방향처럼 게임 규칙으로 검증 가능한 명령을 받아야 한다.

Client Prediction의 구성 요소

입력 시퀀스와 입력 버퍼

각 입력에는 단조 증가하는 번호를 붙인다. 서버는 자신이 마지막으로 처리한 입력 번호를 상태 스냅샷에 함께 실어 보낸다. 클라이언트는 그 번호보다 큰 입력만 아직 서버가 반영하지 않은 입력으로 판단한다.

public struct PlayerInput
{
    public uint Sequence;
    public float MoveX;
    public float MoveZ;
    public bool JumpPressed;
    public float DeltaTime;
}

public struct AuthoritativeState
{
    public uint LastProcessedSequence;
    public Vector3 Position;
    public Vector3 Velocity;
    public bool IsGrounded;
}

입력 버퍼에는 전송한 입력과 해당 입력을 적용한 뒤의 예측 상태를 보관한다. 서버 스냅샷이 도착하면 서버가 이미 처리한 입력을 버퍼에서 제거하고 남은 입력을 권위 상태 위에서 다시 적용한다.

결정적인 이동 시뮬레이션

서버와 클라이언트의 시뮬레이션 결과가 자주 달라지면 정정이 반복되어 캐릭터가 흔들린다. 가능하면 동일한 이동 함수를 두 환경에서 공유하고 고정 시간 간격으로 실행한다.

public static void SimulateMove(ref PlayerState state, in PlayerInput input)
{
    var direction = new Vector3(input.MoveX, 0f, input.MoveZ);
    direction = Vector3.ClampMagnitude(direction, 1f);

    state.Velocity.x = direction.x * state.MoveSpeed;
    state.Velocity.z = direction.z * state.MoveSpeed;

    if (input.JumpPressed && state.IsGrounded)
    {
        state.Velocity.y = state.JumpSpeed;
        state.IsGrounded = false;
    }

    state.Velocity.y += state.Gravity * input.DeltaTime;
    state.Position += state.Velocity * input.DeltaTime;

    // 실제 프로젝트에서는 서버와 클라이언트가 같은 충돌 규칙을 사용해야 한다.
}

부동소수점 연산, 물리 엔진의 접촉 순서, 프레임마다 달라지는 deltaTime은 결과 차이를 키울 수 있다. 완전한 결정성이 어렵다면 정정 자체를 전제로 설계하고 이동 규칙을 단순하게 유지하는 편이 현실적이다. 물리 기반 캐릭터를 그대로 예측하기보다 네트워크용 운동 모델을 별도로 두는 경우가 많은 이유다.

Reconciliation: 되감기와 재실행

서버 상태와 현재 예측 상태의 오차가 허용 범위보다 크면 클라이언트는 다음 순서로 정정한다.

  1. 서버가 보낸 위치, 속도, 접지 상태로 로컬 상태를 복원한다.
  2. 서버가 처리한 입력 번호보다 큰 입력을 버퍼에서 찾는다.
  3. 남은 입력을 오래된 순서부터 다시 시뮬레이션한다.
  4. 화면 표현은 보간하여 위치 변화가 눈에 띄지 않게 완화한다.

위치 오차의 크기는 다음처럼 계산할 수 있다.

error=∥ppredicted−pserver∥error = \lVert p_{predicted} - p_{server} \rVert

error가 아주 작을 때도 매번 되감으면 미세한 떨림이 생길 수 있다. 따라서 작은 오차는 몇 프레임에 걸쳐 시각적 위치만 보정하고 큰 오차나 충돌 상태 불일치에는 즉시 재조정을 적용하는 식으로 나누는 것이 좋다. 단, 렌더링 보간은 표현을 부드럽게 할 뿐 게임 상태의 정합성을 대신하지는 않는다.

다른 플레이어는 보간한다

내 캐릭터는 예측하지만 다른 플레이어는 보통 최근 스냅샷 두 개를 보간하여 표시한다. 아직 도착하지 않은 미래 상태를 기다리기 위해 약간 과거의 시점에서 렌더링하는 방식이다.

이렇게 하면 패킷 도착 간격이 불규칙해도 이동이 부드러워진다. 다만 화면이 현재 서버 상태보다 약간 늦어 보이므로 사격 판정에는 이 시간 차이를 고려해야 한다. 여기서 Lag Compensation이 필요해진다.

Lag Compensation으로 사격 판정하기

Lag Compensation은 서버가 사격 요청을 받았을 때, 사격자가 화면에서 보았을 가능성이 높은 과거 시점의 월드 상태를 잠시 복원해 판정하는 기법이다. 주로 히트스캔 무기에 적합하다.

예를 들어 클라이언트가 100ms 전에 화면에서 적을 조준하고 발사했다면 서버가 요청을 받는 현재 시점의 적 위치만 검사하면 적은 이미 다른 곳으로 이동했을 수 있다. 서버는 사격 시점에 가까운 과거의 적 히트박스로 레이캐스트를 수행해야 한다.

서버가 보관할 이력

서버는 판정 대상의 위치와 회전, 히트박스에 영향을 주는 자세를 짧은 시간 동안 기록한다. 모든 프레임을 저장할 필요는 없지만 서버 틱마다 기록하면 두 기록 사이를 보간할 수 있다.

public struct HitboxSnapshot
{
    public double ServerTime;
    public Vector3 Position;
    public Quaternion Rotation;
    public bool IsCrouching;
}

// 서버 전용: 플레이어별 고정 길이 링 버퍼에 최근 스냅샷을 저장한다.
void RecordHitboxSnapshot(Player player, double serverTime)
{
    player.History.Push(new HitboxSnapshot
    {
        ServerTime = serverTime,
        Position = player.Position,
        Rotation = player.Rotation,
        IsCrouching = player.IsCrouching
    });
}

사격 요청에는 클라이언트가 발사에 사용한 입력 번호 또는 추정 서버 시간을 포함할 수 있다. 그러나 클라이언트가 보낸 시간은 신뢰할 수 없으므로 서버가 추적하는 클라이언트 시간 오프셋과 허용 가능한 최대 보정 시간을 이용해 최종 판정 시각을 제한해야 한다.

bool TryValidateHitscan(FireCommand command, Player shooter)
{
    double requestedTime = ConvertClientTimeToServerTime(
        shooter, command.ClientFireTime);

    double oldestAllowed = ServerTime.Now - MaxRewindSeconds;
    double rewindTime = Math.Clamp(requestedTime, oldestAllowed, ServerTime.Now);

    using var rewind = RewindAllRelevantHitboxes(rewindTime);
    return Physics.Raycast(command.Origin, command.Direction, out var hit, command.Range);
}

RewindAllRelevantHitboxes는 판정이 끝난 뒤 반드시 현재 상태로 되돌려야 한다. 실제 게임에서는 일반 렌더링용 콜라이더와 서버 판정용 히트박스 레이어를 분리하면 되감기 중에 다른 물리 처리에 영향을 줄 위험을 줄일 수 있다.

시간 기준을 명확히 정하기

지연 보정에서 가장 흔한 오류는 서로 다른 시간을 섞는 것이다. 클라이언트의 로컬 시계, 서버 시계, 패킷 도착 시각은 같은 기준이 아니다. 서버는 주기적인 시간 동기화로 클라이언트 시각과 서버 시각의 차이를 추정해야 한다.

단순한 왕복 시간 추정은 다음과 같이 표현할 수 있다.

oneWayDelay≈RTT2oneWayDelay \approx \frac{RTT}{2}

하지만 실제 네트워크는 상행과 하행 지연이 같지 않고 지터도 존재한다. 그러므로 이 값은 정확한 발사 시각이 아니라 보정 범위를 정하는 참고값으로 사용해야 한다. 서버는 너무 오래된 요청을 현재에 가깝게 제한하고 미래 시각은 허용하지 않아야 한다.

치트와 예외 상황 처리

Client Prediction과 Lag Compensation은 반응성을 높이지만 클라이언트를 신뢰하는 구조는 아니다. 다음 검증은 서버에 남겨 두어야 한다.

  • 입력 빈도와 시퀀스 번호가 정상 범위인지 확인한다.
  • 이동 속도, 가속도, 점프 가능 상태를 서버 규칙으로 검증한다.
  • 사격 원점과 방향이 서버가 아는 무기 상태 및 시선 방향과 크게 다르지 않은지 확인한다.
  • 되감기 시간을 최대 보정 시간 안으로 제한한다.
  • 문, 방패, 파괴 가능한 엄폐물처럼 월드 상태가 변하는 대상은 과거 상태까지 기록할지 정책을 정한다.

특히 엄폐물 상태는 설계 결정이 필요하다. 플레이어 히트박스만 되감고 문은 현재 상태로 판정하면 직관과 다른 결과가 나올 수 있다. 반대로 모든 동적 오브젝트를 되감으면 비용과 구현 복잡도가 증가한다. 게임의 무기 특성, 서버 틱, 최대 동시 인원을 기준으로 판정 대상 범위를 명확히 정하는 것이 중요하다.

구현 순서와 점검 항목

처음부터 모든 기능을 붙이기보다 다음 순서로 구현하면 문제를 분리하기 쉽다.

  1. 서버 권위 이동과 스냅샷 전송을 먼저 만든다.
  2. 로컬 플레이어 입력 버퍼와 예측 이동을 추가한다.
  3. 서버 입력 번호를 이용한 되감기와 재실행을 구현한다.
  4. 원격 플레이어 스냅샷 보간을 추가한다.
  5. 서버 히트박스 이력과 히트스캔 되감기 판정을 추가한다.
  6. 지연, 지터, 패킷 손실을 인위적으로 주입해 오차와 판정 결과를 확인한다.

디버그 화면에는 현재 RTT, 입력 버퍼 길이, 마지막 서버 입력 번호, 정정 횟수, 위치 오차, 실제 되감기 시간을 표시하는 것이 좋다. 문제가 생겼을 때 체감만으로 원인을 찾기보다 어느 단계에서 상태가 갈라졌는지 확인할 수 있다.

마무리

Client Prediction은 내 조작을 즉시 보여 주고 서버 정정은 그 결과를 권위 상태에 맞춘다. Lag Compensation은 서버가 늦게 도착한 사격 요청을 과거 월드 상태에서 검증하게 해 준다. 두 기법 모두 서버 권위를 유지하는 것이 핵심이며 입력 시퀀스와 시간 기준, 보정 한계를 명확히 관리할 때 안정적으로 동작한다.