나만의 작은 도서관
[TIL][C++] 250612 MMO 서버 개발 35일차: NDC 영상을 보고 정리한 글 3 - 카트라이더 0.001초 차이의 승부 본문
Today I Learn
[TIL][C++] 250612 MMO 서버 개발 35일차: NDC 영상을 보고 정리한 글 3 - 카트라이더 0.001초 차이의 승부
pledge24 2025. 6. 10. 00:43주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
이번 TIL은 아래 영상을 시청하고 정리한 글입니다.
https://youtu.be/r4ZaolMQOzE?si=08SXwBNggxDpMyQ7
일반적인 Dedicated 동기화 모델(서버-주도 동기화)

- 빌린 데디케이티드 서버에서 서버 프로그램을 실행하는 것으로 시작한다.
- 클라이언트는 서버에게 이벤트를 요청하고,
- 서버는 이벤트를 처리하여 다른 클라이언트에게 Broadcast 한다.
- 이벤트를 전송한 클라이언트도 다른 클라이언트와 같이 Broadcast 된 결과물로 확인할 수 있다는 것이 특징이다.
예시: 미사일 아이템을 사용했을 때

- 공격자가 미사일 발사 이벤트를 서버에게 전송
- 서버는 이벤트를 받고, 공격자가 미사일이 있는지, 피격자가 유효한지 검증(validation)
- 게임 내 모든 유저에게 미사일 발사 이벤트를 Broadcast
- 이후, 서버는 해당 유저가 미사일 피격되기 전까지 실드와 같은 아이템을 사용했는지 파악하여, 피격 시 게임 내 모든 유저에게 미사일 피격 이벤트를 Broadcast
이동 동기화도 미사일 아이템처럼 하면 될까? No!


- 미사일 아이템 동기화 방식을 이동 동기화 방식에도 적용하면, 이동 이벤트를 발생시킨 클라이언트는 서버를 찍고 돌아올 때까지 이동 이벤트에 대한 결과가 처리되지 않는다.
- 대한민국 기준으로 한 클라이언트가 전송한 패킷이 서버에 도착하는 시간은 평균 20ms, 즉, 네트워크 지연은 20*2 = 40ms이다.
- 결국, 본인이 전송한 이동 이벤트의 결과가 다시 나에게 돌아오는 시간은 40ms가 되는데, 이를 60 FPS 기준 프레임 단위로 환산하면 2~3 프레임 정도의 차이가 발생하게 된다.
- 따라서, 아이템 사용의 동기화 방식을 이동 동기화 방식에 그대로 사용하기에는 속도가 매우 빠른 카트라이더에선 사용하기 어려울 수 있다. (프레임 단위로 발생하는 입력 값 차이가 지름길 통과 여부를 가르는데, 정확한 타이밍에 입력했음에도 네트워크 지연 때문에 못 지나갔다면 얼마나 억울하겠는가?)
이동 동기화는 클라-주도 방식으로!

- 따라서, 이동 동기화는 클라이언트 주도 방식을 선택한다. 즉, 클라이언트는 이동의 결과를 즉시 적용하고 서버에겐 이동 결과를 통지한다. 반면, 서버는 통지에 대한 검증(validation)을 한 다음, 다른 클라이언트들에게 동기화 데이터를 Broadcast 한다.
아직 이동 동기화 자체는 해결되지 않았다.
- 서버-주도 동기화 방식에서 클라-주도 동기화 방식으로 바꿨다고 이동 동기화가 해결되지는 않는다.
- 어찌 되었든 서버의 Broadcast로 이동 정보를 받아 이동 동기화를 하는 다른 클라이언트들은, 각기 다른 네트워크 지연에 의해 같은 시각 다른 화면을 바라보게 된다.
- 가장 대표적인 예시로, 각 클라이언트가 동시에 출발했어도 네트워크 지연에 의해 각자의 화면에선 내가 앞서 나가는 것처럼 보인다.
- 게다가 도착한 이동 정보 또한 과거의 정보이기 때문에 이동 패킷을 보낸 클라이언트의 위치랑 또 다르게 된다.
이동 동기화를 하기 위해선 예측해야 한다.
- 다시 한번 말하지만, 다른 상대의 움직임의 변화를 제때 받는 것은 불가능하다. 그래서 어쩔 수 없이 상대의 위치를 예측해 서더라도, 비슷하게라도 그 위치를 알아내야 한다.
- 최소한의 오차로 상대의 위치를 예측하기 위해선 상대의 이전 이동 데이터를 상세히 알고 있어야 한다. 필요한 정보들은 아래와 같다.
- Simulated Result Packet
- Time Stamp: 패킷을 보낸 시간
- Transform(Position, Rotation): 카트의 위치 / 회전값
- Linear Velocity: 선형 속도
- Angular Velocity: 각속도
- Simulated Result Packet
- 추가로, 신뢰성 있는 예측 정보를 제공하기 위해 아래와 같은 기타 기능들도 활용한다.
- Hard Snap
- Validation
Tick Sync

- 만약 C1이 2시에 보냈다는 정보를 패킷을 C2가 2시 2분에 받았다면 C1이 보낸 패킷을 C2가 받는 데까지 걸린 시간은 정확히 2분일까? 아니다. 각 클라이언트는 독립적인 시스템을 가지며, 시스템에서 시간을 측정하는 시계도 서로 다르기 때문에, 각 시계의 시간이 완전히 일치하지 않을 수 있다. 즉 각 시계들은 동시에 타이머를 시작해도,
- 시작 시간이 다를 수 있고,
- 시간이 흐르는 속도가 조금씩 느려지거나 빨라질 수 있다.
왜 이런 일이 발생할까? 클럭 드리프트(clock drift)
컴퓨터는 클럭을 기준으로 시간을 잰다. 즉, 클럭이 돈 횟수가 시간의 단위가 된다는 것이다. 문제는 이 클럭이 항상 정확한 속도로 동작하지 않는다는 것이다. 클럭을 발생시키는 오실레이터가 온도나 압력 등의 외부 요인에 영향을 받게 되면, 클럭 속도가 빨라지거나 느려진다는 것이다.
만약 기존의 클럭 속도보다 빨라지면 시스템 내부의 시간도 빠르게 흐르고, 반대로 클럭 속도보다 느려지면 시간은 느리게 흐르게 된다.
이렇듯 외부 요인에 의해 컴퓨터의 클럭 속도가 달라지면서 시간의 흐름이 점점 느려지거나 빨라질 수 있는데, 이러한 현상을 클럭 드리프트라고 한다.
- 문제는 이러한 클럭 드리프트 현상은 “같은 시점”에 발생한 일이 서로 다르게 해석될 여지를 준다는 것이다. C1이 어떤 이벤트가 “2시에 발생”했다는 정보를 넣어 패킷을 전송해도, C2의 시계에서는 2시가 아닌, “1시 59분”이나 “2시 1분”으로 해석되어 동기화가 제대로 이루어지지 않을 수 있다는 것이다.
클럭 드리프트 현상을 해결하기 위한 방법: Tick Sync

- 이러한 클럭 드리프트 현상을 해결하기 위해 모든 클라이언트가 서버의 시간을 기준으로 삼아 행동하도록 한다. 적용하는 방법은 아래와 같다.
- 서버 타임스탬프 주기 전송
- 서버는 매 틱 마다(ex. 초당 10번 = 10hz) 현재 서버 시간(ReplicatedWorldTimeSeconds)을 포함한 패킷을 모든 클라이언트에게 전송.
- 카트라이더는 언리얼 엔진 자체를 서버에 올린 듯하다.
- 클라이언트는 현재 시점의 본인의 시각(GetServerWorldTimeSeconds)과 비교하여 시차(delta offset)를 계산하고(이 값은 "실제 시차" + "네트워크 지연 시간(RTT의 절반 정도)"을 포함.), 계산한 값을 타임스탬프로 찍어 서버에게 response 패킷을 전송.
- 서버는 매 틱 마다(ex. 초당 10번 = 10hz) 현재 서버 시간(ReplicatedWorldTimeSeconds)을 포함한 패킷을 모든 클라이언트에게 전송.
- 네트워크 지연 계산
- 서버는 각 클라이언트에게 받은 response 패킷을 Broadcast
- C1의 타임스탬프 패킷을 받은 C2는 내부 시스템의 시간과 비교해 진정한 네트워크 지연 시간(PingSeconds)을 계산한다.
extrapolation(외삽법)
위치 외삽법

- 두 클라이언트의 레이턴시가 200ms라면, 200ms 전의 위치일 것이다. 현재의 위치를 알고 싶다면 거속시 공식을 써서 예측 위치(TargetPos)를 구한다. 그렇게 구한 예측 위치는 포지션 interpolation을 통해 이동한다.
- TargetPos는 어디까지나 예측이기 때문에 완전히 믿을 수 없다. 그래서 TargetPos를 그대로 쓰지 않고, 조절 파람(Position_lerp)을 넣어 보간 된 위치를 다음 이동 위치(New Location)로 설정한다.
회전 외삽법

- rotation extrapolation도 마찬가지이다. Location과 같은 방식.
- 각도는 0~360.
속도 외삽법

패킷 지연에 따른 외삽법 적용 테스트
- 언리얼 4에선 이미 콘솔 명령어로 패킷 지연 시간을 설정할 수 있다.

큰 오차가 발생할 땐? HardSnap

- 200ms가 되면 extrepolation이 미래를 예측하는 것이기 때문에, 오차가 발생할 수 있다.
- t초 뒤에 해당 클라이언트가 왼쪽 키를 눌렀는지, 드리프트를 했는지, 서버나 상대 클라는 알 방법이 없음.
- 게다가 카트는 UDP 방식이라서, 중간중간 패킷이 손실될 가능성도 있음.
- 실제값과 예측된 값이 오차가 크거나 오류가 누적되었을 때 HardSnap을 발동시킴.
- HardSnap은 텔레포트되는 현상을 말함
- 이것도 interpolation을 할 수 있긴 한데, 하드 스냅을 사용하는 이유는 유저가 생각했을 때 덜 버그라고 생각했기 때문.
- ⇒ 기존값을 버리고 새로 계산된 값을 바로 쓴다.
- 이것도 interpolation을 할 수 있긴 한데, 하드 스냅을 사용하는 이유는 유저가 생각했을 때 덜 버그라고 생각했기 때문.
파라미터 튜닝: 일부 계수는 상황에 맞게 설정해야 한다.

- 하드 스냅이 자주 발생하면 버그라고 생각하기 쉽다.
- 멀리 있는 유저는 덜 하드 스냅, 가까이 있는 유저는 자주 하드 스냅
- 하드스냅 조건이 각 유저마다 다르게 적용해야 함.
- 굉장히 짧은 순간에 많이 이동하기 때문에 빈번하게 해야 할 것.
- FPS에서 5M 격차가 날 때까지 하드스냅을 발동시키지 않는다? ⇒ 아무도 안 맞을 거임