실시간 PvP 게임은 어떻게 만들어지는가

worldclasscitizen·2026년 4월 22일

SSAFY

목록 보기
7/9

여는 말

실시간 PvP 게임의 핵심 문제는 이것입니다.

여러 플레이어가 같은 게임을 하고 있지만, 각자가 보는 시간은 조금씩 다르다.


내 화면에서는 총알을 피한 것처럼 보여도 상대 화면에서는 맞았을 수 있고, 나는 총을 쐈는데 서버 입장에서는 이미 늦은 입력일 수 있습니다.

그래서 실시간 PvP 게임은 단순히 상태를 주고받는 방식으로 만들 수 없습니다.

반응성, 일관성, 공정성을 모두 고려해야 합니다.


본론

세 가지 목표

게임 네트워킹이 어려운 이유는 세 가지 목표가 서로 충돌하기 때문입니다.

  • 반응성: 키를 누르면 화면이 바로 반응해야 한다.
  • 일관성: 모든 플레이어가 결국 같은 게임 결과를 봐야 한다.
  • 공정성: 특정 플레이어가 네트워크 조건 때문에 지나치게 유리하거나 불리하면 안 된다.

가장 단순한 방식은 클라이언트가 입력을 서버에 보내고, 서버 결과가 돌아올 때까지 기다리는 것입니다.

입력 → 서버 전송 → 서버 판정 → 결과 수신 → 화면 반영

이 방식은 일관성은 좋습니다.

하지만 반응성이 나쁩니다.

왕복 지연 시간이 100ms라면, 플레이어는 키를 누른 뒤 0.1초 후에야 캐릭터가 움직이는 것을 보게 됩니다.

FPS나 대전 게임에서는 이 정도 지연도 크게 느껴집니다.


반대로 클라이언트가 모든 것을 즉시 처리하면 반응성은 좋아집니다.

하지만 각 클라이언트가 자기 판단대로 게임을 진행하기 때문에 상태가 쉽게 어긋납니다.

또 치트에도 취약해집니다.


그래서 현대 실시간 PvP 게임은 보통 다음 구조를 사용합니다.

클라이언트는 먼저 예측하고, 서버는 최종 판정을 내리고, 둘의 차이는 보정한다.


UDP

이 구조를 이해하려면 먼저 왜 게임이 UDP를 주로 쓰는지 봐야 합니다.

TCP는 안정적인 전송을 위해 세 가지를 보장합니다.

  1. 순서 보장
  2. 재전송
  3. 혼잡 제어

웹이나 파일 다운로드에서는 좋은 특성입니다.

하지만 실시간 게임에서는 오히려 문제가 됩니다.


예를 들어 100번 패킷이 유실됐다고 가정해보겠습니다.

TCP는 101번, 102번 패킷이 이미 도착했더라도 100번 패킷이 다시 도착할 때까지 뒤의 데이터를 넘겨주지 않습니다.

게임에서는 과거의 패킷보다 최신 상태가 더 중요합니다.

과거 패킷 하나 때문에 최신 위치 정보가 막히면 화면은 끊기고 입력감은 나빠집니다.


그래서 게임은 보통 UDP를 사용합니다.

UDP는 순서도 보장하지 않고, 재전송도 하지 않습니다.

대신 게임이 필요한 규칙만 직접 정합니다.

  • 위치나 체력 같은 상태는 다음 스냅샷이 덮어쓸 수 있으므로 매번 재전송하지 않는다.
  • 게임 시작, 종료, 중요한 이벤트는 반드시 도착해야 하므로 별도 신뢰성 채널을 사용한다.
  • 입력은 유실되면 문제가 크므로 최근 몇 틱의 입력을 함께 실어 보낸다.

즉, UDP는 불안정해서 쓰는 것이 아니라, 게임이 필요한 방식으로 신뢰성을 직접 설계하기 위해 사용합니다.


권위

다음으로 중요한 것은 누가 진짜 게임 상태를 결정하는가입니다.

이것을 권위 모델이라고 합니다.


순수 P2P 방식에서는 각 클라이언트가 자기 상태를 직접 주장합니다.

구현은 단순하지만 치트에 취약하고, 서로 다른 상태를 주장할 때 해결하기 어렵습니다.


Host Mode에서는 한 클라이언트가 서버 역할도 함께 맡습니다.

개발이 빠르고 별도 서버 비용이 없어서 초기 MVP에 적합합니다.

하지만 호스트가 치트를 쓰면 막기 어렵고, 호스트가 나가면 세션이 흔들릴 수 있습니다.


Dedicated Server 방식에서는 게임에 참여하지 않는 별도 서버가 권위를 가집니다.

모든 클라이언트는 서버에 입력을 보내고, 서버는 판정 결과를 다시 내려줍니다.

운영 비용은 들지만 공정성과 통제력이 좋습니다.


정리하면 이렇습니다.

  • 빠른 개발과 낮은 비용이 중요하면 Host Mode가 유리하다.
  • 공정성, 치트 방어, 운영 안정성이 중요하면 Dedicated Server가 유리하다.

Colosseum은 초기 단계에서는 Host Mode를 사용할 수 있지만, 장기적으로는 Server Mode 전환을 고려해야 하는 구조입니다.


틱

권위 구조가 정해졌다면, 다음 문제는 시간입니다.

Unity의 Update()는 프레임마다 호출됩니다.

하지만 프레임은 기기 성능에 따라 달라집니다.

어떤 클라이언트는 60FPS로 돌고, 어떤 클라이언트는 144FPS로 돌 수 있습니다.

이런 프레임 시간을 기준으로 네트워크 게임을 동기화하면 각자의 시간이 달라집니다.


그래서 Fusion은 틱을 사용합니다.

틱은 네트워크에서 공유하는 고정된 시간 단위입니다.

Unity Update    : |--|-|---|--|-----|--|
Fusion Tick     : |---|---|---|---|---|
                   T0  T1  T2  T3  T4

FixedUpdateNetwork()는 이 틱마다 호출됩니다.

줄여서 FUN()이라고 부릅니다.


여기서 중요한 규칙이 있습니다.

FUN() 안에서는 Time.deltaTime이 아니라 Runner.DeltaTime을 사용해야 한다.


Time.deltaTime은 프레임 기준 시간입니다.

프레임은 매번 달라질 수 있기 때문에 네트워크 시뮬레이션 기준으로는 안정적이지 않습니다.

반면 Runner.DeltaTime은 Fusion의 틱 간격과 맞는 값입니다.

모든 피어가 같은 기준으로 계산하려면 틱 기준 시간을 사용해야 합니다.


예측

이제 다시 반응성 문제로 돌아올 수 있습니다.

클라이언트가 서버 결과를 기다리기만 하면 입력이 늦게 반영됩니다.

그래서 Fusion은 클라이언트 예측을 사용합니다.

흐름은 이렇습니다.

  1. 플레이어가 키를 누른다.
  2. 클라이언트는 입력을 서버에 보내면서, 동시에 로컬에서 먼저 움직인다.
  3. 서버는 입력을 받아 권위 있는 상태를 계산한다.
  4. 서버 결과가 클라이언트로 돌아온다.
  5. 클라이언트는 자기 예측과 서버 결과를 비교한다.

둘이 같으면 그대로 진행합니다.

다르면 서버 상태로 되돌린 뒤, 그 이후 입력을 다시 적용합니다.

이 과정을 롤백 재시뮬레이션이라고 합니다.


예를 들어 클라이언트가 T=100부터 T=105까지 미리 예측했다고 가정해보겠습니다.

그런데 서버가 뒤늦게 T=101의 권위 상태를 보내왔고, 클라이언트의 예측과 달랐습니다.

그러면 클라이언트는 T=101로 되돌아갑니다.

그 다음 T=102부터 T=105까지 저장해둔 입력을 다시 실행합니다.

예측 진행:
T100 → T101 → T102 → T103 → T104 → T105

서버 상태 도착:
        S101

재시뮬레이션:
        S101 → T102' → T103' → T104' → T105'

이 구조 덕분에 플레이어는 입력이 즉시 반응하는 것처럼 느낍니다.

동시에 최종 결과는 서버 권위에 맞춰집니다.


결정성

롤백이 제대로 동작하려면 중요한 조건이 있습니다.

같은 입력을 다시 실행했을 때 같은 결과가 나와야 합니다.

이것을 결정성이라고 합니다.


예를 들어 FUN() 안에서 Random.Range()를 그냥 호출하면 문제가 생길 수 있습니다.

처음 실행했을 때와 재시뮬레이션 때의 랜덤 결과가 달라질 수 있기 때문입니다.

Time.deltaTime을 사용해도 마찬가지입니다.

프레임 시간이 달라지면 같은 입력을 넣어도 결과가 달라질 수 있습니다.


그래서 FUN() 안에서는 다음을 조심해야 합니다.

  • Random.Range() 같은 비결정적 호출
  • Time.deltaTime 사용
  • 순서가 보장되지 않는 컬렉션 순회
  • Update()에서 Networked 상태 변경
  • 사운드, 파티클 같은 부작용의 중복 실행

사운드나 파티클은 게임 결과를 바꾸는 상태가 아닙니다.

이런 연출은 재시뮬레이션 때마다 반복 실행되면 안 됩니다.

그래서 실제 진행 시점인지 확인한 뒤 한 번만 실행해야 합니다.


보간

로컬 플레이어는 예측할 수 있습니다.

내 입력을 내가 알고 있기 때문입니다.

하지만 상대 플레이어는 예측하기 어렵습니다.

상대가 어떤 키를 눌렀는지 즉시 알 수 없고, 서버 스냅샷도 일정 간격으로만 도착합니다.

게다가 네트워크 지터 때문에 도착 간격도 일정하지 않습니다.


그래서 원격 객체는 보통 예측하지 않고 보간합니다.

핵심은 살짝 과거를 보여주는 것입니다.

클라이언트는 서버 스냅샷을 받자마자 바로 표시하지 않고, 짧게 버퍼에 쌓아둡니다.

그리고 두 스냅샷 사이를 부드럽게 이어서 보여줍니다.

서버 스냅샷:
T100 -------- T101 -------- T102

클라이언트 표시:
   T99.5 ---- T100.5 ---- T101.5

이렇게 하면 스냅샷 하나가 조금 늦게 도착해도 화면이 덜 끊깁니다.

대신 원격 플레이어는 실제 서버 상태보다 약간 과거의 모습으로 보입니다.


즉, 로컬 플레이어는 예측으로 반응성을 확보하고, 원격 플레이어는 보간으로 부드러움을 확보합니다.


상태와 이벤트

네트워크로 무엇을 보낼지도 중요합니다.

모든 것을 같은 방식으로 보내면 안 됩니다.

게임 데이터는 크게 두 가지로 나눌 수 있습니다.


상태는 계속 유지되는 값입니다.

예를 들어 위치, 체력, 탄약, 라운드 점수 같은 값입니다.

이런 값은 [Networked] 속성으로 동기화하는 것이 자연스럽습니다.

public class Player : NetworkBehaviour
{
    [Networked] public byte Health { get; set; }
    [Networked] public Vector2 AimDir { get; set; }
}

상태는 다음 스냅샷이 이전 값을 덮어쓸 수 있습니다.

그래서 한 번 유실되어도 다음 값이 오면 복구됩니다.


반대로 이벤트는 한 번 발생하고 끝나는 일입니다.

예를 들어 승리 연출, 게임 시작 신호, 채팅 메시지 같은 것들입니다.

이런 것은 RPC로 처리할 수 있습니다.

[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
public void RPC_ShowVictoryFlash(PlayerRef winner)
{
    // 모든 클라이언트에서 승리 연출 실행
}

하지만 모든 이벤트를 RPC로 보내면 안 됩니다.

매 틱 변하는 위치를 RPC로 보내면 대역폭이 커지고, 델타 압축의 장점도 사라집니다.


기준은 단순합니다.

지속되는 값은 Networked 상태로, 드물게 발생하는 일회성 신호는 RPC로 처리한다.


게임 결과에 영향을 주는 값은 가능한 서버 권위 상태로 남겨야 합니다.

반대로 카메라 흔들림, 파티클, 사운드 같은 연출은 로컬에서 처리해도 됩니다.


품질

네트워크 품질은 보통 네 가지로 봅니다.


레이턴시는 지연 시간입니다.

입력이 서버에 도착하고 결과가 돌아오는 데 걸리는 시간입니다.

레이턴시가 높으면 입력 반응이 늦게 느껴집니다.


지터는 패킷 도착 간격의 흔들림입니다.

평균 핑이 낮아도 지터가 크면 화면이 끊길 수 있습니다.

일정한 지연은 보간 버퍼로 흡수하기 쉽지만, 들쭉날쭉한 지연은 더 어렵습니다.


패킷 손실은 패킷이 유실되는 비율입니다.

Fusion은 유실에 강한 구조를 갖고 있지만, 연속으로 스냅샷이 유실되면 보간 버퍼가 비고 화면이 끊길 수 있습니다.

입력 패킷이 유실되면 재시뮬레이션 결과도 흔들릴 수 있습니다.


대역폭은 단위 시간당 전송되는 데이터 양입니다.

Networked 속성이 많아도 자주 변하지 않으면 큰 문제가 아닐 수 있습니다.

반대로 매 틱 변하는 값은 계속 전송되므로 대역폭에 직접 영향을 줍니다.


그래서 최적화할 때는 Networked 속성의 개수만 볼 것이 아니라, 얼마나 자주 변하는지를 봐야 합니다.


문제

실무에서 자주 만나는 문제도 대부분 앞의 원리에서 이어집니다.


러버밴딩은 클라이언트 예측과 서버 판정이 크게 어긋날 때 발생합니다.

클라이언트는 앞으로 갔다고 예측했는데, 서버는 그 위치를 인정하지 않으면 캐릭터가 뒤로 끌려오는 것처럼 보입니다.

이를 줄이려면 클라이언트와 서버가 같은 시뮬레이션 코드를 사용해야 하고, FUN() 안의 결정성을 지켜야 합니다.


상태 불일치는 클라이언트마다 서로 다른 게임 상태를 믿는 문제입니다.

원인은 대체로 Networked 상태를 잘못 다루거나, 이벤트와 상태의 경계를 흐린 경우입니다.

예를 들어 이펙트는 떴는데 체력은 줄지 않았다면, 플레이어는 "맞았는데 왜 체력이 그대로지?"라고 느낍니다.

그래서 게임 결과에 영향을 주는 정보와 단순 연출 정보를 분리해야 합니다.


치트는 권위 모델과 직접 연결됩니다.

상태 권한이 서버에 있으면 클라이언트가 체력을 마음대로 바꾸기 어렵습니다.

하지만 Host Mode에서는 호스트 클라이언트가 서버 역할을 겸하기 때문에, 호스트 본인의 치트는 근본적으로 막기 어렵습니다.

이 문제를 해결하려면 결국 Dedicated Server 구조가 필요합니다.


재연결도 따로 고려해야 합니다.

짧은 끊김은 잠시 유예하고 복구를 기다릴 수 있습니다.

하지만 긴 끊김은 라운드 패배, 세션 이탈, AI 대체 같은 게임별 정책이 필요합니다.

네트워크 프레임워크가 연결 복구를 도와줄 수는 있지만, 게임 상태를 어떻게 처리할지는 결국 게임 규칙의 문제입니다.



결론

Colosseum 기준

Colosseum 같은 실시간 PvP 게임에서는 모든 정보를 네트워크로 보내는 것이 답이 아닙니다.

보내야 하는 것과 보내지 않아도 되는 것을 나눠야 합니다.


네트워크로 동기화해야 하는 값은 게임 결과를 바꾸는 값입니다.

  • 플레이어 위치
  • 체력
  • 탄약
  • 투사체 위치
  • 반사 횟수
  • 라운드 상태
  • 카드 선택 결과

반대로 로컬에서 처리해도 되는 값도 있습니다.

  • 카메라 흔들림
  • 사운드
  • 파티클
  • 트레일 색상
  • HP 바 애니메이션
  • 히트스톱 연출

이 경계를 잘못 잡으면 네트워크 문제는 점점 복잡해집니다.

상태로 보내야 할 것을 연출로 처리하면 게임 결과가 어긋납니다.

로컬로 충분한 것을 Networked로 만들면 대역폭과 관리 비용이 늘어납니다.


그래서 기준은 이렇게 잡는 것이 좋습니다.

게임 결과를 바꾸는 것은 서버 권위 상태로 남기고, 보여주는 방식만 로컬에서 처리한다.


설계 기준

Fusion 기반 PvP 게임을 만들 때는 다음 순서로 생각하는 것이 좋습니다.

  1. 누가 권위를 가질지 정한다.
  2. 어떤 값이 게임 결과를 바꾸는지 정한다.
  3. 그 값만 Networked 상태로 만든다.
  4. 입력은 클라이언트가 제출하고 서버가 검증한다.
  5. 로컬 플레이어는 예측한다.
  6. 원격 플레이어는 보간한다.
  7. 서버 결과와 다르면 롤백으로 보정한다.
  8. 연출은 가능한 로컬에서 처리한다.

이 흐름을 지키면 구조가 단순해집니다.

클라이언트는 반응성을 책임지고, 서버는 판정을 책임지고, Fusion은 그 사이의 차이를 보정합니다.


최종 구조

결국 실시간 PvP 게임의 핵심은 다음 문장으로 정리할 수 있습니다.

클라이언트는 예측하고, 서버는 판정하고, 차이는 다시 보정한다.


이 문장을 기준으로 보면 각 기술의 역할도 명확해집니다.

  • UDP는 최신 상태를 빠르게 주고받기 위한 선택이다.
  • 틱은 모두가 공유하는 시간 기준이다.
  • 예측은 입력 지연을 숨기기 위한 방법이다.
  • 롤백은 예측과 서버 결과의 차이를 줄이기 위한 방법이다.
  • 보간은 원격 객체를 부드럽게 보여주기 위한 방법이다.
  • Networked 상태와 RPC 구분은 무엇을 지속 상태로 볼지 정하는 문제다.
  • Server Mode는 공정성과 치트 방어를 강화하기 위한 구조다.


닫는 말

처음에는 실시간 PvP 게임이 단순히 데이터를 빠르게 주고받는 문제처럼 보였습니다.

하지만 실제로는 시간, 권위, 예측, 보정, 연출을 함께 설계해야 하는 문제였습니다.

네트워크 지연은 없앨 수 없습니다.

대신 플레이어가 지연을 덜 느끼게 만들고, 최종 결과는 서버 기준으로 맞춰야 합니다.


Fusion 2는 이 과정을 도와주는 프레임워크입니다.

하지만 어떤 값을 Networked로 만들지, 어떤 이벤트를 RPC로 보낼지, 어떤 연출을 로컬에 둘지는 여전히 개발자가 결정해야 합니다.

그래서 중요한 것은 API를 외우는 것보다 구조를 이해하는 것입니다.


최종적으로 이 글의 결론은 다음과 같습니다.

실시간 PvP 게임은 빠른 통신만으로 만들어지지 않는다.
예측 가능한 클라이언트, 권위 있는 서버, 그리고 둘을 이어주는 보정 구조로 만들어진다.

0개의 댓글