본문 바로가기

네트워크/게임 서버 이론 (Game Server Theory)

Fast-Paced Multiplayer (Part II): Client-Side Prediction and Server Reconciliation 해석

원문 : https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html

 

Client-Side Prediction and Server Reconciliation - Gabriel Gambetta

Fast-Paced Multiplayer (Part II): Client-Side Prediction and Server Reconciliation Client-Server Game Architecture · Client-Side Prediction and Server Reconciliation · Entity Interpolation · Lag Compensation · Live Demo Introduction In the first article of

www.gabrielgambetta.com

본 게시물은 Gabriel Gambetta가 쓴 Fast-Paced Multiplayer를 해석한 글입니다. 원문과 내용은 거의 같으나 일부 각색이 들어갈 수 있습니다.

혹시 읽으시면서 틀리거나 이상한 부분이 있으면 꼭 댓글에 적어주세요.

 

 

Introduction


이 시리즈의 첫 번째 글에서는 authoritative server를 이용한 클라이언트-서버 모델을 살펴보았다. 이 방식으로만 그대로 적용시키면 유저의 입력이 게임 화면에 바로 나타나지 않는 입력 지연을 겪게 된다. 

 

지연이 생길 수 있는 인터넷과 같은 네트워크 환경에서는 게임의 반응이 느린 거는 그나마 나은 정도고, 최악에는 진행이 불가능할 정도가 될 수도 있다. 이번 글에서는 그런 문제를 최소화하거나 아예 해결해버리는 방법에 대해서 다룰 것이다.

Client-side prediction


비록 핵 유저가 있더라도, 게임 서버는 대부분 정상적인 요청(requests)을 받을 것이다. 이것을 다르게 보면, 대부분의 입력 요청들은 서버에 의해 인증될 것이고 예상한 대로 게임 상태를 갱신할 것이다. 만약 우리가 (10, 10)에서 오른쪽 이동 버튼을 누른다면 결국엔 (11, 10)으로 갈 거라는 것이다.

 

만약 게임이 결정론적(deterministic)이라면 우리는 예상대로 움직인다라는 점을 이용할 수 있다. 여기서 말하는 결정론적이란 게임 상태와 입력이 주어진다면 그 결과를 완벽하게 예측이 가능하다라는 뜻이다.

 

먼저 서버와의 지연이 100ms가 걸린다 가정하고, 게임 캐릭터의 이동 애니메이션이 100ms가 걸린다고 가정해보자. 아무런 처리를 하지 않는다면 이동키를 누르고 이동이 끝날 때까지 총 200ms가 걸릴 것이다.

게임이 결정론적이라면, 우리가 보낸 입력이 서버에서 정상적으로 적용될 거라는 것을 예상할 수 있다. 그러므로 클라이언트는 입력이 적용된 후의 게임 상태를 예측할 수 있고, 이건 대부분 맞을 것이다. 

 

이제 입력을 보내고 랜더링(rendering)할 새로운 게임 상태를 기다리는 대신에, 입력을 보내고 그게 적용된 게임 상태를 랜더링하는 것이다.

그러면 드디어 authoritative server를 사용하면서도 입력 지연이 없어지는 것을 볼 수 있다. 만약 해킹당한 클라이언트가 비정상적인 입력을 보내고 그걸 바로 랜더링 했다고 해도, 자신의 클라이언트에서만 그렇게 보일 뿐이지 서버에서는 해당 입력을 무시할 것이기 때문에 다른 클라이언트에는 영향이 없을 것이다.

Synchronization issues


사실 위의 예제에서는 모든 것이 잘 작동하도록 숫자를 골랐다. 이제 숫자를 조금 바꾼 경우를 보도록 하자. 서버와의 지연이 250ms이 걸리고, 이동하는 애니메이션은 똑같이 100ms이 걸린다고 가정한다. 또한 유저가 오른쪽 이동키를 2번 연속으로 눌렀다고 해보자.

 

위의 Client-side prediction을 사용한다면 아래와 같은 상황이 벌어질 것이다.

새로운 게임 상태가 도착하는 t = 250ms인 지점에서 재미있는 일이 일어난다. 클라이언트가 예측한 게임 상태는 x = 12이지만, 서버에서 x = 11인 게임 상태가 도착한다. Authoritative server 방식이기 때문에 클라이언트는 캐릭터를 x = 11로 움직일 수밖에 없다. 그러다 t = 350에 서버에서 x = 12인 게임 상태가 오게되고, 캐릭터는 다시 x = 12로 이동한다.

 

유저의 입장에서 보면, 오른쪽 이동키를 2번 눌렀고 캐릭터는 바로 2칸 오른쪽으로 이동했다. 그리고 50ms 있다가 갑자기 왼쪽으로 한 칸 순간이동했다가, 100ms 후에 또 갑자기 오른쪽으로 한 칸 순간이동하는 것을 보게 된다. 당연하지만, 이건 용납할 수 없는 일이다.

Server reconciliation


침착하게 다시보면 클라이언트는 게임의 현재를 보고있는데, 지연으로 인해 서버가 게임의 과거를 보내주는 게 이 문제의 핵심이다. 클라이언트가 서버에게서 받은 게임 상태는 이미 전에 처리되었기 때문에 이런 동기화 문제가 생기게 된 것이다.

 

다행히 이건 해결하기 그리 어려운 문제는 아니다. 먼저 클라이언트는 각 요청에 번호(sequence number)를 붙인다. 예를 들어, 첫 번째 키 입력 요청에는 #1번을, 두 번째 키 입력 요청에는 #2번을 붙인다. 그러고 나서 서버가 응답할 때는 마지막으로 처리된 번호를 넣어서 보낸다.

이제 클라이언트가 서버로 보낸 요청들의 복사본을 가지고 있다고 하자. t = 250에 도착한 새로운 게임 상태를 보고 서버에서 #1 요청이 처리됬다는 것을 알 수 있다. 그렇다면 해당 복사본을 버릴 수 있다. 하지만 서버에게서 받아야 할 #2 요청이 있다는 것도 알고 있다. 그러므로 서버에서 온 마지막 상태와 서버가 아직 처리하지 않은 입력을 기반으로 현재 게임 상태를 예측하여 client-side prediction을 계속 적용시킨다.

 

계속해서 t = 350에 서버로부터 새로운 게임 상태가 도착한다. x = 12이고 #2 요청이 처리됬다는 걸 알게 되었다. 클라이언트는 #2까지의 모든 입력 요청들을 버리고, 게임 상태를 x = 12로 갱신한다. 이제 더이상 응답받을 입력이 없으므로 모든 처리는 정확한 게임 상태와 함께 끝나게 된다.

Odds and ends


위의 예제는 이동만을 표현했지만, 같은 원리로 다른 동작에도 적용시킬 수 있다. 예를 들어, 턴제 대전 게임에서 유저가 상대방 캐릭터를 공격했다면 피격 효과와 대미지 숫자를 볼 수는 있겠지만, 실제로는 서버에서 응답이 올 때까지는 캐릭터의 체력을 깎아서는 안된다.

 

게임 상태의 복잡성으로 인하여, 심지어 자신의 클라이언트에서 체력이 0이하로 떨어지는 것으로 나오더라도, 서버가 그러라 하기 전까지는 캐릭터를 없애는 것은 피해야 한다. 만약 그 캐릭터가 치명타를 맞기 전에 체력 회복을 했는데, 서버가 그 사실을 아직 안 알려줄 수도 있기 때문이다.

 

이건 우리에게 흥미로운 주제를 제시한다. 게임이 완벽하게 결정론적이고 핵이 없더라도, 클라이언트가 예측한 게임 상태와 서버가 보낸 게임 상태가 서로 다를 수 있다는 점이다. 싱글 플레이라면 그럴 일은 없겠지만, 여러 유저가 한 서버에 연결되었다면 얘기가 달라진다. 이 주제에 관해서는 다음 글에서 다룰 예정이다.

Summary


Authoritative server를 사용할 때, 우리는 유저에게 서버가 입력을 처리하는 동안 유저에게 반응성의 환상을 보여주어야 한다. 이것을 실현하기 위해서 클라이언트는 입력의 결과를 보여준다. 서버로부터 게임 상태가 도착하면 클라이언트의 예측된 상태는 도착한 게임 상태와 서버로부터 아직 처리되지 않은 입력을 갖고 다시 계산된다.

 

ps. 이번 글에서는 매끄러운 설명을 위해서 표현을 다르게 한 부분이 약간 있습니다. 내용상 큰 차이는 없지만 궁금하신 분들은 원문을 봐주세요.