delay-based와 rollback 넷코드
실시간 멀티플레이어가 네트워크 지연을 숨기는 전략 입력을 늦추는 delay-based와 예측 후 되감는 rollback을 원리부터 비교
Contents
delay-based와 rollback은 다른 기술이 아니라, 같은 결정론 토대 위에서 원격 입력을 '기다리느냐 예측하느냐'로 갈리는 두 전략이다.
왜 넷코드가 필요한가
실시간 멀티플레이어의 핵심 난제는 여러 기계가 같은 게임 세계를 보게 만드는 상태 동기화다. 어려운 이유는 네트워크가 불안정하기 때문이다. 보낸 정보는 지연되거나, 순서가 뒤바뀌거나, 사라질 수 있다.
세 결함을 구분해 두면 이후 설명이 쉽다. 지연(latency)은 패킷이 한 기계에서 다른 기계까지 가는 시간이고, 손실은 UDP에서 패킷이 사라지는 것, 지터(jitter)는 지연의 분산이다. Glenn Fiedler는 "인터넷은 초당 60번 보낸 패킷이 1/60초 간격으로 곱게 도착한다고 보장하지 않는다"고 정리한다(gafferongames). 평균 핑이 같아도 도착 간격이 출렁이면 화면이 끊겨 보인다.
입력만 보내는 lockstep과 결정론
초기 P2P 게임은 게임 상태 전체가 아니라 각 플레이어의 입력만 교환했다. 이것이 락스텝(lockstep)이다. 1993년 Doom은 1 tic(약 28.57ms)마다 입력을 모든 피어에 보내고, 모든 입력이 도착해야 그 tic을 진행했다.
입력만 주고받는데도 모든 기계가 같은 화면을 보려면 전제가 하나 필요하다. 같은 시작 상태에 같은 입력을 먹였을 때 모두가 비트 단위로 같은 결과를 내야 한다. 이것이 결정론(determinism)이다. 이 원칙이 깨지면 한 기계의 미세한 차이가 시간이 지나며 완전한 desync(동기화 깨짐)로 번진다.
결정론이 보장되면 네트워크로는 입력만 보내면 되고, 대역폭이 오브젝트 수가 아니라 입력 크기에 비례한다. 유닛이 수천 개인 실시간 전략 게임(real-time strategy, RTS)이 지금도 이 모델을 쓰는 이유다.
여기까지가 두 방식의 공통 토대다. 게임은 고정 프레임레이트로 돌고(격투게임 60fps, 1프레임 ≈ 16.67ms), 시뮬레이션은 결정론적이며, 네트워크로는 입력만 보낸다. 갈림길은 단 하나, 원격 입력이 도착할 때까지 무엇을 하느냐다.
위 그림은 두 전략의 결정 지점을 한눈에 보여준다. delay-based는 기다리고, rollback은 예측한 뒤 틀린 경우에만 되감는다.
delay-based 넷코드
delay-based는 결정론적 락스텝에 입력 지연 버퍼를 얹은 형태다. 핵심은 자신의 입력조차 의도적으로 몇 프레임 늦춰 실행하는 것이다. 내 입력을 즉시 반영하지 않고 D프레임 뒤에 실행하기로 약속하면, 그 D프레임 동안 입력 패킷이 상대에게 도착할 시간이 생긴다.
결과적으로 양쪽 시뮬레이션은 매 프레임 두 입력이 모두 갖춰진 상태에서만 한 프레임씩 전진한다. 추측도 되감기도 없으니 양쪽 상태는 항상 동일하다. 지연 프레임 수는 편도 지연을 프레임 시간으로 나눠 정한다. 핑이 90ms이면 편도는 평균 45ms이고, 45ms ÷ 16.67ms ≈ 약 3프레임의 입력 지연이 필요하다(jy-h 예시). 지터를 견디려 1~2프레임을 더 얹기도 한다.
장점은 분명하다. 상태 저장이 필요 없어 구현이 단순하고, 전제는 결정론 하나뿐이며, 시각적 깨짐이 없다. Age of Empires는 유닛 상태 대신 명령만 전달해 28.8k 모뎀에서 1500유닛을 동기화했다(Bettner & Terrano, GDC 2001).
단점은 모두 "모든 입력이 도착할 때까지 진행할 수 없다"는 동기 제약에서 나온다. 핑이 높을수록 입력 랙이 그대로 체감되고, 패킷이 늦거나 유실되면 화면 전체가 멈추며(stutter), 전체 속도가 가장 느린 피어에 묶인다. 콤보처럼 프레임 단위 타이밍이 생명인 격투게임에서 이 입력 랙은 치명적이다.
rollback 넷코드
rollback은 로컬 입력을 지연 0으로 즉시 반영하고, 아직 도착하지 않은 상대 입력은 "직전 입력을 그대로 반복한다"고 가정해 게임을 멈추지 않는다. 격투게임은 입력이 매 프레임 바뀌지 않으므로 이 단순 가정의 적중률이 높다. 네 부분이 맞물려 돌아간다.
- 입력 예측(prediction): 상대 입력이 오기 전 직전 입력을 가정해 진행한다. '무행동 유지' 예측이 표준이다.
- 상태 스냅샷(state snapshot): 매 프레임의 게임 상태 전체를 기록해 둔다. GGPO(Good Game Peace Out)의
save_game_state콜백이 상태 구조체를 통째로 복사하는 부분이다. 이것이 흔히 말하는 '롤백 레코드'의 실체다. - 입력 링버퍼(ring buffer): 과거 입력과 스냅샷을 롤백 윈도우 크기로 고정한 원형 버퍼에 보관한다. NetherRealm은 7프레임을 지원하므로 7칸 링버퍼를 쓴다(Stallone, GDC 2018).
- 재시뮬레이션(re-simulation): 실제 입력이 예측과 다르면, 일치하던 가장 최근 프레임의 스냅샷으로 되감아 실제 입력으로 현재 프레임까지 다시 돌린다. 예측이 맞았으면 아무 일도 하지 않는다.
구현
상태가 정수 위치 하나뿐인 결정론적 시뮬레이션으로 예측·롤백·재시뮬을 재현해 본다. 상대 입력은 delay 프레임 늦게 도착하고, 그동안은 직전 확정 입력으로 예측한다. 예측이 빗나가면 스냅샷으로 되감아 다시 돌린다.
NEUTRAL = 0
def step(pos, p1, p2):
"""결정론적 1프레임 전진. 같은 (pos, p1, p2)면 언제 호출해도 같은 결과."""
return pos + p1 + p2
def run(frames, p1, p2, delay):
snap = [0] * frames # 각 프레임 진입 직전 상태 = 롤백 레코드(스냅샷)
used_p2 = [None] * frames # 각 프레임에서 적용한 P2 입력 = 입력 링버퍼
pos = 0
confirmed = NEUTRAL # 가장 최근 도착한 P2 입력 = 예측의 근거
rollbacks = 0
for f in range(frames):
arrived = f - delay # delay 프레임 전의 P2 입력이 지금 도착
if arrived >= 0 and used_p2[arrived] != p2[arrived]:
rollbacks += 1
pos = snap[arrived] # 스냅샷 복원
confirmed = p2[arrived]
for g in range(arrived, f): # 확정 프레임부터 직전까지 재시뮬
snap[g] = pos
pg = p2[g] if g == arrived else confirmed
used_p2[g] = pg
pos = step(pos, p1[g], pg)
elif arrived >= 0:
confirmed = p2[arrived]
snap[f] = pos # 현재 프레임은 예측으로 진행(지연 0)
used_p2[f] = confirmed
pos = step(pos, p1[f], confirmed)
return pos, rollbacksp1 = [+1,+1,+1,+1,+1,0,0], p2 = [0,0,+1,+1,0,0,0], delay = 2로 돌리면 출력은 다음과 같다.
f4: P2[f2] 도착=+1 != 예측=+0 -> f2로 롤백 후 2프레임 재시뮬
f6: P2[f4] 도착=+0 != 예측=+1 -> f4로 롤백 후 2프레임 재시뮬
롤백 횟수 = 2
rollback 최종 pos = 7
정답(지연 없음) pos = 7상대 입력이 2프레임 늦게 도착하므로 예측이 두 번 빗나가고, 그때마다 해당 프레임으로 되감아 2프레임씩 다시 돌린다. 모든 입력이 확정된 뒤 최종 상태(7)는 지연이 전혀 없었을 때의 정답(7)과 비트 단위로 일치한다. 결정론이 보장되면 되감아 다시 돌려도 같은 결과로 수렴한다는 것이 rollback의 정확성 근거다.
비용과 체감
모든 재시뮬레이션은 화면 한 프레임 예산(60Hz = 16.66ms) 안에 끝나야 한다. NetherRealm의 발표 제목 '8 Frames in 16ms'가 이 뜻이다. 7프레임을 되감아 7개의 경량 시뮬레이션과 1개의 렌더를 한 프레임 안에 처리한다(Stallone, GDC 2018). 예산을 넘기면 다음 프레임에 더 많이 되감아야 하는 죽음의 나선(spiral of death)에 빠진다.
사용자 입장에서 입력 반응성은 로컬 지연이 0이라 오프라인과 거의 같다. 비용은 예측이 크게 빗나갔을 때의 순간 보정(pop)이지만, 실제로는 대부분의 매치가 1~2프레임 롤백에 그쳐 시각적 아티팩트가 거의 없다(Stallone). GPU 파티클이나 사운드처럼 비결정적 요소는 8번 다시 돌리면 결과가 달라지므로, 게임 결과에 영향이 없는 한 재시뮬에서 빼고 캐시한다.
두 방식 비교
| 축 | delay-based | rollback |
|---|---|---|
| 원격 입력 대기 | 도착까지 대기(내 입력도 D프레임 지연) | 예측해 즉시 진행, 틀리면 재시뮬 |
| 로컬 반응성 | 핑에 비례해 나빠짐 | 항상 0지연 |
| 두 클라이언트 상태 | 매 프레임 항상 동일 | 예측 중에는 다를 수 있음 |
| 시각적 아티팩트 | 없음 | 예측 빗나가면 pop(대개 미미) |
| 패킷 지연·유실 시 | 화면 전체가 멈춤 | 원격 캐릭터만 살짝 글리치 |
| 필요 자료구조 | 입력 버퍼만 | 상태 스냅샷 + 입력 링버퍼 |
| CPU 비용 | 낮음 | 높음(재시뮬, 나선 위험) |
| 잘 맞는 곳 | RTS, 저지연 LAN | 격투게임, 핑 변동 큰 인터넷 |
트레이드오프는 한 줄로 요약된다. delay-based는 일관성을 위해 반응성을 희생하고, rollback은 반응성을 위해 순간적 일관성을 희생한다.
어디에 쓰이는가
넷코드는 격투게임만의 것이 아니다. 진실을 누가 소유하는가로 크게 세 계열로 나뉜다.
P2P 결정론 계열(격투의 rollback, RTS의 lockstep) 은 중앙 서버 없이 결정론 자체를 동기화 수단으로 쓴다. 격투게임은 2020년 코로나19로 대회가 온라인으로 옮겨가며 rollback이 사실상 표준이 됐다. Guilty Gear Strive(2021), Street Fighter 6(2023), Tekken 8(2024)이 모두 출시 시점 rollback으로 나왔다. RTS는 유닛이 수천 개라 상태 스냅샷이 비현실적이므로 lockstep을 유지한다.
권위 서버 계열(FPS, MOBA) 은 다르다. 1인칭 슈팅 게임(first-person shooter, FPS)은 서버가 유일한 진실을 쥐고 클라이언트를 신뢰하지 않으며, 기계 간 결정론이 필요 없다. 대신 클라이언트측 예측, 서버 조정(reconciliation), 지연 보상(lag compensation), 엔티티 보간을 쌓는다(Valve Source 문서). 이 예측은 rollback과 닮았지만, FPS는 자기만 예측하고 서버가 확정하면 그쪽으로 보정될 뿐이라는 점이 다르다.
선택을 가르는 변수는 플레이어 수, 동기화 단위 규모, 트위치 반응 요구, 부정행위 모델이다. 격투는 2인에 작은 상태라 rollback, RTS는 수천 유닛이라 lockstep, FPS는 상금이 걸린 경쟁과 안티치트 때문에 권위 서버를 고른다.
직접 구현할 때의 벽
GGPO의 세 전제(완전 결정론, 직렬화 가능한 상태, 렌더 없이 1프레임 save/load/advance)는 각각 별개의 난제로 갈라진다.
결정론이 가장 깊은 벽이다. 부동소수점 연산은 컴파일러와 CPU에 따라 미세하게 달라 desync를 낸다. 그래서 다수의 결정론 게임이 부동소수점을 버리고 고정소수점으로 시뮬레이션을 다시 쓴다. 난수 생성기(random number generator, RNG)의 내부 상태도 저장 상태에 포함해야 같은 프레임을 몇 번 되감아도 같은 난수열이 재생된다.
상태 직렬화는 자료구조 설계 문제다. 이상적 형태는 전체 게임 상태를 하나의 큰 구조체에 연속 배치해 복사 한 번으로 저장·복원하는 것이다. 동적 할당이 있으면 포인터를 인덱스로 바꿔야 한다. 매 틱 전체 스냅샷이 비싸면 변경분만 저장하는 증분 롤백으로 비용을 줄인다.
이 전제들이 코드 전반에 스며들기 때문에 나중에 끼워넣기가 매우 어렵다. NetherRealm은 Mortal Kombat X에 rollback을 후행 이식하며 약 10개월에 8 man-year를 썼다(직렬화에만 2 man-year). 반대로 처음부터 GGPO를 전제로 설계한 Skullgirls의 Mike Zaimont는 약 2주 만에 통합했다(Stallone, GDC 2018). 같은 기능이 설계 시점에 따라 2주와 8 man-year로 갈린다.
정리
delay-based와 rollback은 고정 프레임레이트, 결정론, 입력 전송이라는 같은 토대 위에서 원격 입력을 기다리느냐 예측하느냐로 갈리는 두 전략이다. delay-based는 양쪽 상태를 항상 일치시키는 대신 핑만큼 입력을 늦추고, rollback은 로컬 반응성을 0지연으로 지키는 대신 예측이 빗나간 순간을 되감아 다시 돌린다. 흔히 말하는 '롤백 레코드'는 rollback이 보관하는 상태 스냅샷과 입력 링버퍼를 가리키며, delay-based에는 그런 기록이 없다.
같은 실시간 동기화 문제를 장르가 다르게 푼다는 점도 중요하다. 격투는 rollback, RTS는 lockstep, FPS와 MOBA는 권위 서버에 예측·보정·지연 보상을 얹는다. 무엇을 고를지는 플레이어 수, 상태 규모, 반응 요구, 부정행위 모델이 결정한다. 결국 rollback을 쓸지 말지는 장르가 아니라 '상태가 작고 결정론과 직렬화를 처음부터 깔 수 있는가'라는 아키텍처 질문으로 환원된다.