플레이어가 스킬 버튼을 누른 순간부터 다른 플레이어 화면에 그 스킬이 보이기까지 패킷이 지나는 단계를 내 클라, 네트워크, 서버, 네트워크, 상대 클라 순서로 보여준다. 예시 조건(20Hz, RTT 50ms)에서 전체 합계는 약 221ms이고, 내 화면에서는 약 25ms 만에 보이는 스킬이 상대 화면에는 200ms 넘게 걸려 나타난다. 이벤트도 보간 시점에 맞춰 재생하는 구현이라면 가장 큰 몫은 상대 클라의 보간 버퍼, 네트워크 편도 두 번(내 업링크, 상대 다운링크), 서버 틱 대기다.
화면에는 다섯 개 구간이 나란히 놓인다.
- 내 클라: 입력, 예측 실행, 패킷 생성,
send()
- 네트워크: 공유기·와이파이, ISP, 인터넷 백본, 방화벽·LB
- 게임 서버: I/O 스레드, 존 메시지 큐, 틱에서 검증·판정, AOI·브로드캐스트
- 네트워크: 백본, 상대 ISP, 상대 공유기
- 상대 클라: recv, 보간 버퍼, 애니메이션·이펙트, 화면
패킷은 내 클라에서 UseSkillReq로 출발하고, 서버에서 처리된 뒤 SkillCastNoti로 바뀌어 상대 클라에 도착한다. 아래쪽에는 이 구간이 업링크, 서버 처리, 다운링크로 묶인다. 구간마다 시간이 조금씩 쌓이고, 그 합이 상대가 느끼는 지연이다.
내 클라 안의 단계는 키 입력(~0ms), 게임 루프(0~16ms), 예측 실행(즉시), 패킷 생성(<1ms), 직렬화·암호화(<1ms), send()(<1ms), NIC(<1ms) 순서다. 키 입력은 OS 이벤트로 들어오고, 게임 루프가 다음 프레임에 읽기 때문에 최대 한 프레임(60fps 기준 16ms)을 기다릴 수 있다.
예측 실행 단계에서 클라이언트는 서버 승인을 기다리지 않고 스킬 애니메이션을 먼저 재생한다. 그래서 내 화면은 이미 스킬을 쓰는 중이다.
패킷은 game_packet.proto의 UseSkillReq 메시지로 만들어진다. 필드는 seq(입력 순번), skill_id, target_id, pos(내가 본 내 위치), client_ms(보낸 시각)다. seq와 client_ms는 나중에 위치 보정과 지연 측정에 쓰인다. 여기까지 1~20ms가 걸리고, 대부분은 다음 프레임을 기다리는 시간이다.
패킷은 내 PC에서 출발해 공유기·와이파이, ISP 망, 인터넷 백본, IDC 방화벽·DDoS 필터, 로드밸런서를 지나 게임 서버에 닿는다. 각 지점 위의 누적 시간은 0, 2, 8, 16, 23, 24, 25ms이고, 편도 합계는 25ms다. 편도는 대략 RTT의 절반이며, 여기서는 RTT 50ms를 예시로 잡았다.
- 와이파이 구간: 지터와 손실이 가장 많이 생긴다.
- 거리: 광섬유 안의 빛도 1,000km를 가는 데 약 5ms가 걸린다.
- 방화벽·LB: 보통 1ms 미만이지만 트래픽이 몰리면 큐잉이 생긴다.
- 손실이 나면: TCP 재전송으로 수십 ms가 늘고, RTO에 걸리면 200ms 이상 멈춘다.
서버에 도착한 패킷은 NIC에서 커널 수신 버퍼로, 다시 I/O 스레드로 넘어가 recv, 복호화, 역직렬화를 거친다. 세션 확인으로 누구의 패킷인지 정한 뒤 존 메시지 큐에 들어간다. 앞의 단계는 각각 <1ms지만, 큐에서는 다음 틱이 시작될 때까지 0~50ms를 기다린다.
틱 처리는 로직 스레드에서 약 12ms 동안 진행된다. 쿨다운, 사거리, 마나, 치트 여부를 검증하고, 데미지와 상태 변경을 판정한다. AOI 조회로 주변 플레이어(화면 예시는 32명)를 찾고, SkillCastNoti와 DamageNoti를 브로드캐스트한다. 결과는 송신 큐를 거쳐 I/O 스레드에서 직렬화, 암호화, send된다.
아이템, 로그 같은 DB 기록은 비동기로 처리되어 틱이 기다리지 않는다. 서버 구간에서 가장 긴 시간은 처리보다 다음 틱까지의 대기이고, 평균은 틱 간격의 절반이다.
서버가 보낸 패킷은 다운링크(상대 회선 기준 ~25ms)를 지나 상대 클라의 NIC와 recv(<1ms)에 도착한다. 게임 루프가 다음 프레임에 읽고(0~16ms), 보간 버퍼(~100ms), 애니메이션·이펙트(즉시), vsync에 맞춘 화면 출력(~16ms)을 거친다.
보간 버퍼는 서버 스냅샷 사이를 부드럽게 잇기 위해 상대 클라가 일부러 약 100ms 과거의 상태를 그리는 장치다. 화면에서 이 단계만 빨간색으로 강조되어 있다.
아래 타임라인은 같은 스킬이 각 화면에 보이는 시점이다. 내 화면에서는 입력 대기와 렌더를 거쳐 약 25ms에 시전이 시작되고(예측), 서버 판정(72ms)이 다운링크로 돌아와 그려지는 약 120ms에 데미지가 확정되며, 상대 화면에는 221ms에 내 스킬이 보인다.
버튼부터 상대 화면까지 시간을 예시 조건(20Hz, RTT 50ms)으로 더하면 다음과 같다. 입력·처리 10ms, 업링크 25ms, 틱 대기 25ms, 틱 처리 12ms, 다운링크 25ms, 프레임 8ms, 보간 버퍼 100ms, 렌더 16ms로 합계는 약 221ms다.
- 보간 버퍼 ~100ms: 부드러운 움직임의 대가다. 지터가 크면 더 늘린다.
- 네트워크 편도×2 ~50ms: 내 업링크와 상대 다운링크의 합이다. 거리, 회선, 와이파이에 따라 달라진다.
- 틱 대기 + 처리 ~37ms: 틱레이트가 낮을수록, 서버가 바쁠수록 늘어난다.
- 클라 프레임 ~25ms: FPS가 낮으면 입력과 표시가 모두 늦어진다.
막대 아래에는 구간별 묶음이 표시된다. 내 클라 10ms, 업링크 네트워크 25ms, 서버 37ms, 다운링크 네트워크 25ms, 상대 클라 124ms(프레임, 보간 버퍼, 렌더)다. 상대 클라 구간이 가장 크고, 그 대부분이 보간 버퍼다.
보간 버퍼는 위치·상태 스냅샷에 쓰인다. 시전 같은 이벤트는 받자마자 재생하는 게임도 많고, 그때는 이 100ms가 빠진다. 위 합계는 이벤트도 보간 시점에 맞춰 재생하는 구현을 가정한 값이다.
용어 정리
- 예측 실행 (Client-side Prediction)
- 클라이언트가 서버 승인을 기다리지 않고 입력 결과를 먼저 화면에 보여주는 기법이다. 내 화면이 서버 응답을 기다리지 않고 반응하는 이유다.
- UseSkillReq / SkillCastNoti
- 클라가 서버에 스킬 사용을 요청하는 패킷과, 서버가 주변 플레이어에게 스킬 시전을 알리는 패킷이다. 영상에서 쓰는 예시 메시지 이름이다.
- 직렬화
- 메모리 안의 구조체를 네트워크로 보낼 바이트 열로 바꾸는 일이다. 영상에서는 protobuf를 쓴다.
- RTT
- 패킷이 상대에게 갔다가 응답이 돌아오는 왕복 시간이다. 편도 지연은 대략 RTT의 절반으로 본다.
- 지터
- 패킷 도착 간격이 고르지 않게 흔들리는 정도다. 와이파이 구간에서 크게 나타나며, 크면 보간 버퍼를 늘려야 한다.
- 틱 / 틱레이트
- 틱은 서버가 월드를 한 번 갱신하는 주기, 틱레이트는 초당 틱 횟수다. 20Hz면 틱 간격이 50ms다.
- 틱 대기
- 요청이 존 메시지 큐에 들어가 다음 틱이 시작될 때까지 기다리는 시간이다. 0~50ms 사이로, 평균은 틱 간격의 절반이다.
- 존 메시지 큐
- 존별로 들어온 요청을 로직 스레드가 다음 틱에 꺼내 처리하도록 쌓아 두는 대기열이다.
- AOI
- 각 플레이어가 보는 범위, 즉 시야다. 서버는 이 범위 안의 플레이어에게만 스킬 시전 사실을 보낸다.
- 브로드캐스트
- 판정 결과를 AOI 안의 여러 플레이어에게 전송하는 일이다.
- 보간 버퍼
- 상대 클라가 서버 스냅샷을 일정 시간 모아 두고, 그 사이를 부드럽게 이어 그리기 위해 일부러 과거 상태를 보여주는 버퍼다. 영상 예시는 약 100ms다.
- vsync
- 모니터 갱신 주기에 맞춰 프레임을 출력하는 방식이다. 60Hz 화면이면 최대 약 16ms를 더 기다릴 수 있다.
- RTO
- TCP가 ACK를 기다리다 타이머가 만료되어 재전송하는 방식이다. 리눅스 최솟값이 200ms라 손실 한 번이 큰 정지로 이어진다.