멀티플레이어 핑 렉 보정: Client Prediction과 Rollback 구현 가이드
멀티플레이어 게임에서 핑으로 인한 입력 지연과 판정 불일치를 줄이는 Client Prediction, 서버 권위 검증, Rollback 재시뮬레이션 구현 원리와 튜닝 기준을 정리합니다.

핵심 요약
멀티플레이어 게임의 핑 렉은 네트워크 왕복 시간(RTT)만큼 서버 응답을 기다릴 때 입력이 늦게 보이는 문제다. Client Prediction은 로컬 입력을 즉시 실행해 조작감을 유지하고 서버 권위 상태가 도착하면 저장해 둔 입력을 다시 적용하는 Rollback으로 서버와 클라이언트의 상태를 일치시킨다.
핵심은 클라이언트를 신뢰하지 않는 것이다. 클라이언트는 예측해 보여 주되 최종 판정과 검증은 서버가 수행하며 불일치가 발생한 틱부터 결정론적으로 재시뮬레이션한다.
멀티플레이어 게임에서 핑 렉은 왜 발생하는가?
플레이어가 이동 키를 누른 뒤 서버가 이를 받고 결과를 다시 보내려면 네트워크 왕복 시간이 필요하다. 서버 응답 뒤에만 캐릭터를 움직이면 화면상 입력 지연은 대략 RTT에 비례한다.
예를 들어 RTT가 100ms이고 서버가 60Hz로 동작한다면 플레이어는 약 100ms 이상 늦게 움직이는 캐릭터를 보게 된다. 액션 게임에서는 50ms 내외의 지연도 조작감에 영향을 줄 수 있다.
| 방식 | 화면 반응 | 치트 방어 | 상태 일치 | 적합한 장르 |
|---|---|---|---|---|
| 서버 응답 대기 | 느림 | 강함 | 높음 | 턴제, 느린 전략 게임 |
| Client Prediction만 사용 | 즉시 | 약함 | 시간이 갈수록 어긋날 수 있음 | 간단한 협동 게임 |
| Prediction + Rollback | 즉시 | 강함 | 높음 | 슈팅, 스포츠, 액션 |
| P2P 롤백 | 즉시 | 낮음 | 높음 | 대전 격투 게임 |
서버 권위형 구조에서는 클라이언트가 보낸 좌표를 그대로 받아들이면 순간이동, 속도 핵, 벽 통과에 취약해진다. 따라서 서버는 좌표가 아니라 입력 명령과 입력이 생성된 틱을 받고 자체 시뮬레이션으로 다음 상태를 계산해야 한다.
Client Prediction은 어떻게 동작하는가?
Client Prediction은 내 캐릭터의 입력을 서버 확인 전에 로컬에서 먼저 시뮬레이션하는 방식이다. 예측 상태는 임시 결과이고 서버 스냅샷은 언제나 정답이다.
flowchart LR
I[로컬 입력: tick 120] --> C[클라이언트 즉시 시뮬레이션]
I --> N[입력 명령을 서버로 전송]
N --> S[서버 권위 시뮬레이션]
S --> A[권위 스냅샷: ack tick 120]
A --> R[클라이언트 보정 및 재시뮬레이션]
R --> C
클라이언트는 매 시뮬레이션 틱마다 다음 데이터를 기록한다.
- 입력 시퀀스 번호 또는 틱 번호
- 방향, 점프, 발사처럼 서버에 보낼 입력 명령
- 입력 적용 뒤의 예측 상태
입력 명령은 조작 의도를 표현해야 한다. position = (10, 3)처럼 결과 좌표를 보내는 대신 moveX, moveY, jumpPressed처럼 재현 가능한 입력을 전송한다.
public struct PlayerInput
{
public uint Tick;
public sbyte MoveX;
public sbyte MoveY;
public bool JumpPressed;
}
public struct PlayerState
{
public uint Tick;
public Vector3 Position;
public Vector3 Velocity;
public bool Grounded;
}

서버 권위와 Rollback은 어떻게 구현할까?
Rollback은 서버 스냅샷을 받은 뒤 해당 스냅샷의 확인 틱 이후에 아직 서버가 처리하지 않은 로컬 입력을 다시 적용하는 과정이다. 이때 물리 규칙, 고정 시간 간격, 난수 처리가 클라이언트와 서버에서 일관되어야 한다.
Step 1. 고정 틱과 입력 버퍼를 설계한다
서버와 클라이언트는 예를 들어 60Hz의 고정 틱을 사용한다. 각 클라이언트는 최근 입력을 링 버퍼에 저장하고 서버는 플레이어별로 마지막 처리 입력 틱을 관리한다.
버퍼 길이는 예상 최대 RTT와 지터를 고려해 잡는다. 60Hz에서 2초를 보관하려면 최소 120개 틱이 필요하다. 실제 서비스에서는 재전송, 일시적인 패킷 손실, 보정 여유를 고려해 128 또는 256개처럼 2의 거듭제곱 크기를 자주 사용한다.
const int HistorySize = 128;
PlayerInput[] inputHistory = new PlayerInput[HistorySize];
PlayerState[] stateHistory = new PlayerState[HistorySize];
void SimulateLocalTick(PlayerInput input)
{
uint tick = input.Tick;
inputHistory[tick % HistorySize] = input;
predictedState = Simulate(predictedState, input, 1f / 60f);
predictedState.Tick = tick;
stateHistory[tick % HistorySize] = predictedState;
SendInputUnreliable(input);
}
Step 2. 서버가 입력을 검증하고 권위 스냅샷을 보낸다
서버는 입력 틱의 허용 범위, 입력 값 범위, 행동 쿨다운, 이동 속도를 검증한 뒤 동일한 Simulate 규칙으로 상태를 갱신한다. 너무 오래된 입력을 무제한으로 수용하면 과거 상태를 반복적으로 계산해야 하므로 허용 가능한 지연 창을 명확히 제한해야 한다.
func (s *Server) ProcessInput(player *Player, input PlayerInput) {
if input.Tick <= player.LastProcessedTick {
return // 중복 또는 역순 패킷
}
if input.Tick > s.CurrentTick {
return // 이 예제에서는 아직 도달하지 않은 틱의 입력을 처리하지 않음
}
if abs(input.MoveX) > 1 || abs(input.MoveY) > 1 {
return // 입력 범위 검증
}
player.State = Simulate(player.State, input, fixedDeltaTime)
player.LastProcessedTick = input.Tick
}
스냅샷에는 최소한 서버 상태와 마지막으로 처리한 입력 틱(ackTick)이 포함되어야 한다. ackTick이 없으면 클라이언트는 어느 입력까지 서버가 반영했는지 알 수 없다.
Snapshot {
serverTick: 135,
ackTick: 120,
position: (12.4, 0.0, -8.1),
velocity: (3.0, 0.0, 0.0),
grounded: true
}
Step 3. 서버 상태로 되돌린 뒤 미확인 입력을 재시뮬레이션한다
클라이언트가 ackTick = 120인 서버 스냅샷을 받았다면 120번 틱의 예측 상태와 비교한다. 차이가 허용 오차보다 크면 서버 상태로 되돌리고 121번 틱부터 현재 틱까지 저장된 입력을 순서대로 재적용한다.
void ApplyAuthoritativeSnapshot(PlayerState serverState, uint ackTick)
{
PlayerState predictedAtAck = stateHistory[ackTick % HistorySize];
if (predictedAtAck.Tick != ackTick)
{
RequestFullResync(); // 버퍼에 해당 틱이 없으면 오래된 슬롯을 사용하지 않음
return;
}
float positionError = Vector3.Distance(
predictedAtAck.Position,
serverState.Position
);
float velocityError = Vector3.Distance(
predictedAtAck.Velocity,
serverState.Velocity
);
const float reconcileThreshold = 0.05f;
if (positionError <= reconcileThreshold &&
velocityError <= reconcileThreshold &&
predictedAtAck.Grounded == serverState.Grounded)
return;
predictedState = serverState;
for (uint tick = ackTick + 1; tick <= localTick; tick++)
{
PlayerInput input = inputHistory[tick % HistorySize];
if (input.Tick != tick)
continue;
predictedState = Simulate(predictedState, input, 1f / 60f);
predictedState.Tick = tick;
stateHistory[tick % HistorySize] = predictedState;
}
}
보정이 화면에서 튀지 않게 하려면?
시뮬레이션 상태와 렌더링 상태를 분리한다. 게임 규칙에는 보정된 권위 상태를 즉시 반영하지만 화면 모델은 짧은 시간 동안 오차를 보간해 이동시키면 순간 이동처럼 보이는 현상을 줄일 수 있다.
다만 보간은 시각적 처리일 뿐이다. 충돌 판정, 발사 위치, 이동 가능 여부 같은 게임플레이 계산에 보간 좌표를 사용하면 다시 상태 불일치가 생긴다.
| 상황 | 권장 처리 |
|---|---|
| 오차가 매우 작음 | 무시하거나 짧게 보간 |
| 일반적인 위치 오차 | Rollback 후 렌더링 위치만 보간 |
| 텔레포트, 리스폰, 넉백 | 즉시 반영 또는 별도 연출 |
버퍼에 ackTick 기록이 없음 | 최신 서버 상태로 재동기화 요청 |
| 반복되는 큰 보정 | 틱 불일치, 충돌 규칙, 부동소수점 차이 조사 |
히트 판정에도 Rollback을 써야 하는가?
대부분의 실시간 슈팅 게임에서는 그렇다. 사격 요청이 서버에 도착한 시점의 월드로만 판정하면 고핑 플레이어는 화면에서 조준한 위치와 다른 결과를 받는다. 서버는 발사 입력의 추정 서버 틱 또는 클라이언트 시간을 검증 가능한 범위로 변환하고 해당 시점의 다른 플레이어 히트박스를 과거 위치로 되감아 레이캐스트한다.
이를 서버 측 Lag Compensation 또는 히트박스 Rewind라고 부른다. 플레이어 이동의 Client Prediction과 목적은 비슷하지만 적용 대상이 다르다.
- 이동 Rollback: 내 캐릭터의 예측 상태를 서버 권위 상태에 맞춘다.
- 히트박스 Rewind: 서버가 과거 시점의 상대 히트박스로 공격을 판정한다.
과거 상태 보관 기간은 공격 장르와 공정성 정책에 따라 제한한다. 예를 들어 최대 200ms 또는 250ms까지만 되감으면 고핑 사용자가 지나치게 오래된 화면을 기준으로 공격하는 문제를 완화할 수 있다. 클라이언트가 보낸 시간값은 위조될 수 있으므로 서버가 측정한 RTT와 허용 창으로 반드시 검증해야 한다.
결정론적 시뮬레이션에서 자주 발생하는 문제
부동소수점 오차
서버가 Go나 C++이고 클라이언트가 Unity C#이라면 부동소수점 연산 순서와 물리 엔진 구현 차이로 같은 입력에도 결과가 달라질 수 있다. 경쟁성이 높은 게임은 고정소수점 수학, 자체 이동 컨트롤러, 명시적인 충돌 순서를 고려할 수 있다.
물리 엔진 전체를 재시뮬레이션하는 문제
엔진 물리 월드를 그대로 Rollback하면 다른 오브젝트의 순서와 접촉 상태까지 재현해야 한다. 먼저 플레이어 이동에 한정된 간단한 운동학 컨트롤러부터 구현하고 예측 대상과 비예측 대상을 분리하는 편이 안전하다.
패킷 손실과 입력 누락
입력 패킷은 보통 UDP 또는 신뢰성 없는 전송으로 보내므로 일부가 유실될 수 있다. 각 패킷에 최신 입력뿐 아니라 직전 2~3개 입력을 중복 포함하거나 시퀀스 번호 기반 재전송 전략을 사용한다. 서버는 같은 입력을 여러 번 받아도 한 번만 처리해야 한다.
자주 묻는 질문 (FAQ)
Client Prediction만으로 충분한가?
아니다. 예측만 하면 클라이언트마다 서로 다른 상태를 오래 유지할 수 있다. 서버 스냅샷과 Rollback 재시뮬레이션으로 권위 상태에 맞춰야 한다.
모든 오브젝트를 Rollback해야 하는가?
아니다. 일반적으로는 로컬 플레이어와 즉시 반응이 필요한 객체만 예측한다. 다른 플레이어와 대부분의 월드 오브젝트는 서버 스냅샷을 보간해 표시하는 편이 비용과 복잡도 면에서 유리하다.
TCP로도 Client Prediction과 Rollback을 구현할 수 있는가?
구현은 가능하지만 TCP의 헤드오브라인 블로킹은 빠른 상태 업데이트에 불리할 수 있다. 실시간 입력과 스냅샷에는 대개 UDP 또는 채널별 신뢰성 정책을 지원하는 전송 계층을 사용한다.
정리
핑 렉 보정의 목표는 네트워크 지연을 없애는 것이 아니라 지연이 있어도 조작감과 공정성을 함께 유지하는 것이다. 로컬에서는 즉시 예측하고 서버에서는 입력을 검증해 권위 상태를 계산한 뒤 클라이언트는 확인 틱 이후의 입력을 재시뮬레이션한다.
처음부터 모든 물리 객체를 롤백하려 하기보다 고정 틱 기반의 플레이어 이동, 입력 버퍼, ackTick이 포함된 서버 스냅샷, 오차 로그부터 구현하자. 이 네 가지가 안정적으로 동작하면 히트박스 Rewind와 예측 가능한 투사체 같은 고급 기능을 단계적으로 추가할 수 있다.



