WILD BOWL : ZOOPORTS 게임에서 Client-Side Prediction 적용기

Jong Ha Shin
6 min readJan 7, 2025

들어가며

Fast-Paced 장르의 멀티플레이 게임을 만들 때 가장 자주 부딪히는 문제 중 하나가 서버와의 네트워크 지연입니다. 사용자 입장에서는 즉각적인 반응이 보여야 하는데, 서버 권한(Authoritative Server) 구조를 쓰면 클라이언트는 서버 응답을 기다려야 하기 떄문이죠. 이 문제를 해결하기 위한 대표적인 방법이 Client-Side PredictionServer Reconciliation입니다.
아래에서는 직접 시도했던 구현 과정과 해결 방안을 간단히 공유해보겠습니다!

문제/원인

  • 서버에서 모든 인풋을 받아 처리하면 치팅(핵)에 대한 대응은 쉬움. 그러나 클라이언트는 서버 응답을 기다려야 해서 조작감(조작 지연)이 떨어짐.
  • 네트워크 지연이 100ms 정도만 되어도, FPS나 MOBA 같은 장르는 크게 체감하게 됨.
  • 클라에서 먼저 이동해놓고 서버에 통보하는 구조로 바꾸면, 좌표 싱크(서버와 위치 차이) 문제가 발생함.
  • 여러 인풋을 빠르게 입력하는 상황에서는, 서버에서 받는 “과거 상태” 때문에 캐릭터가 순간적으로 되돌아왔다가 다시 이동하는 등의 문제가 발생함.

해결

Client-Side Prediction

  • 클라이언트가 인풋을 넣자마자 캐릭터를 미리 움직임.
  • 서버 역시 인풋을 받고 권한에 따라 결과를 계산함. 이후 결과를 클라에 전송함.
  • 이후 클라는 서버 결과값과 자신의 ‘예측값’을 비교해 오차를 보정함(Server Reconciliation).

Server Reconciliation

  • 클라이언트에서 보내는 인풋을 순번(예: InputNumber)으로 관리함.
  • 서버가 해당 순번까지 처리했다고 알려주면, 클라에선 그 순번까지의 인풋을 삭제하고, 이후 쌓인 인풋만 다시 예측해 적용함.
  • 이를 통해 클라-서버 위치 불일치를 줄임.

Entity Interpolation (다른 플레이어의 움직임)

  • 내 캐릭터는 즉시 예측 이동. 하지만 다른 플레이어의 움직임은 100ms 전 상태를 적절히 보간해서 보여줌.
  • Dead Reckoning, Interpolation 등을 혼합하여 자연스러운 움직임을 구현함.
  • FPS의 경우 에임, 헤드샷 판정 등의 시점 오차는 별도 Lag Compensation이 필요함.

예시 코드

아래 코드는 Mirage 네트워킹 라이브러리와 Unity 환경에서 Client-Side Prediction, Server Reconciliation을 구현했던 실제 예시입니다.

/// <summary>
/// 플레이어의 이동을 컨트롤하는 스크립트
/// </summary>
public sealed class CharacterMoveController : NetworkBehaviour
{
// 코드 내 주요 포인트
// - 클라이언트가 _moveInfoList에 InputNumber와 방향을 저장함.
// - 서버는 인풋을 받아 권한 서버에서 처리 후 위치를 모든 클라이언트에 전송함.
// - 클라에서는 OnDeserialize() 시점에 서버로부터 받은 lastInputNumber를 확인해
// 그 인풋까지의 데이터를 제거하고 나머지를 다시 적용해 예측 위치를 보정함.
// - Run(), Turn() 등에서 캐릭터 이동/회전 시뮬레이션을 수행함.

private struct SMoveInputData
{
public uint InputNumber;
public Vector3 Direction;
}

private List<SMoveInputData> _moveInfoList = new List<SMoveInputData>(30);
private Vector3 _clientRecvPos; // 서버가 알려준 실제 위치
private Vector3 _clientSimulatedPos; // 클라이언트가 예측 적용한 위치

// ...
[ServerRpc]
private void ServerRpcSendMoveInputData(SMoveInputData sMoveInputData)
{
// 서버에서 인풋 수신 -> 권한에 따라 실제 이동 계산
// 이후 전체 클라이언트에게 위치값 전송
}

public override void OnDeserialize(NetworkReader reader, bool initialState)
{
// 서버에서 위치, 마지막 처리된 인풋 번호(lastInputNumber)를 받아옴
// _moveInfoList에서 해당 번호까지 삭제 -> 나머지 인풋 다시 적용
// 예측 위치 보정
}

[Client]
private void ClientReSimulation()
{
// 서버에서 받은 위치 (_clientRecvPos) 기준으로
// 남아있는 인풋을 다시 적용해 클라이언트 예측 위치를 업데이트
}

// ...
}

핵심 로직 흐름

  • 클라에서 인풋(Key, Joystick 등) 생성 -> ServerRpcSendMoveInputData()로 서버에 보냄.
  • 클라에서도 Run(), Turn()으로 ‘예측 이동’을 즉시 수행함.
  • 서버는 받은 인풋을 순차적으로 처리 -> 최종 위치를 모든 클라에게 전송. 실제 프로젝트에서는 별도로 위치 동기화를 하는 클래스가 따로 존재했습니다.
  • 클라는 받은 위치를 기준점으로 삼고, 아직 서버에 반영되지 않은 인풋만 재적용하여 보정함.

마무리

Client-Side Prediction은 서버 권한을 유지하면서도 플레이어에게 ‘끊김 없는 움직임’을 제공하려는 목적입니다. 구현 과정은 복잡해 보이지만, 핵심은 “클라는 미리 움직이고, 서버의 신뢰성 있는 결과로 결국 보정한다”로 요약할 수 있습니다.
Dead Reckoning, Interpolation, Lag Compensation 등은 여기서 자세하게 소개하지는 않았지만, “내 캐릭터”뿐만 아니라 “상대 캐릭터”의 움직임이나 히트 판정 같은 민감한 처리까지 부드럽게 만들기 위한 기술들입니다.

실제 제가 참여했던 Wild Bowl : ZOOPORTS의 경우 미국에서 주로 서비스를 했었는데, 한국 클라이언트로 미국 서버에 접속했었어도 최소 이동 등의 유저 경험은 아주 부드럽게 동작하도록 구현할 수 있었습니다.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response