Source Multiplayer Networking

 

원문 : http://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking

번역 : http://vegadanet.tistory.com

 

Overview


 ‘Source Engine’을 사용한 멀티플레이 게임은 서버-클라이언트 구조를 사용합니다. 여기서 서버는 시뮬레이션, 판정, 플레이어의 입력처리 등을 담당합니다. 클라이언트와 서버는 서로 작은 패킷을 굉장히 자주(초당 20~30번 정도) 교환하면서 통신합니다. 클라이언트는 서버로부터 게임의 현재 상태를 받으면서 이를 화면에 표현해 줍니다. 또한 클라이언트는 사용자의 키보드, 마우스, 마이크 등의 입력을 받아 서버로 보내는 역할도 합니다. 클라이언트는 오직 서버와 통신할 뿐 다른 클라이언트와는 통신하지 않습니다. 즉 P2P 모델을 사용하지 않습니다. 싱글플레이 게임과는 대조적으로 멀티플레이 게임은 네트워크로 인해 발생되는 다양한 문제점등을 해결해야 합니다.

 

 네트워크 대역폭은 한계가 있습니다. 따라서 서버는 플레이어들의 모든(!) 상태변화에 대한 패킷을 보내줄 수 없습니다. 그 대신, 서버는 특정 시점에서 플레이어의 상태를 받아 이를 다른 플레이어들에게 알려줍니다. 서버와 클라이언트 사이에는 단연코 지연이 생기게 됩니다. 따러서 서버에서 처리하는 패킷은 이미 유저의 과거의 명령이게 됩니다. 게다가 클라이언트마다 서로 다른 네트워크 환경으로 인한 다른 지연시간을 갖습니다. 이 같은 서버와 클라이언트 사이의 시간 오차로 문제점이 발생합니다. 빠른 응답시간을 요하는 게임에서 수 천분의 1초는 게임의 렉을 느끼게 할 수 있고 다른 플레이어나 움직이는 물체와 상호 반응하는데 어려움을 만들 수 있습니다. 게다가 대역폭 한계와 네트워크 지연으로 인한 패킷 손실로 플레이 정보가 손실 될 수도 있습니다.

사용자 삽입 이미지

 Source engine은 위와 같은 문제점을 풀기 위해 여러 가지 테크닉들을 사용합니다. 이 테크닉들은 문제점을 완벽하게 해결하지는 못하더라도 적어도 플레이어가 문제점을 느끼지 못하게끔 합니다. 이 테크닉들은 data compression, interpolation, prediction, lag compensation 입니다. 이 문서는 이 테크닉들에 대한 일반적인 방법론들에 관해 다룰 것 입니다.

 

Basic Networking


 서버는 시간에 대하여 tick 이라고 불리는 불연속적인 시간의 순서로 게임을 시뮬레이션 합니다. 기본적으로 초당 66 tick으로 시뮬레이션이 진행되지만 이는 변경이 가능합니다. 예를 들어 카운터스트라이크는 서버의 CPU 부하를 줄이기 위해 초당 33 tick 이라는 비교적 낮은 수치로 시뮬레이션을 합니다. 각 tick 동안 서버는 유저의 입력을 받아서 물리계산, 판정, 그리고 상태들을 업데이트 합니다. 한 tick의 시뮬레이션이 끝난 후, 서버는 업데이트가 필요한 클라이언트를 찾아서 업데이트 시킵니다. Tick 의 빈도수가 높을수록 더욱 정확한 시뮬레이션을 할 수 있지만, 그만큼 더욱 CPU의 부하를 일으키고 더욱 많은 서버와 클라이언트의 대역폭을 사용합니다. Tick 의 빈도수는 –tickrate 명령을 사용하여 변경이 가능하지만 이 기능이 완벽하게 작동되지 않을 수 있기 때문에 사용하지 않는 것을 추천합니다.

 

 클라이언트는 보통 대역폭의 제한이 있습니다. 가장 나쁜 환경은 5~7 KB/sec 이상 받지 못하는 모뎀을 사용하는 사용자 입니다. 만약 서버가 이 사용자들에게 높은 빈도수로 업데이트를 한다면 많은 패킷들이 손실될 것입니다. 그래서 클라이언트는 자신의 네트워크 성능(byte/second)을 서버에게 알려줘야 합니다. 이 성능은 최적의 게임을 하는데 있어 가장 중요한 요소입니다. 서버는 클라이언트의 이 성능이상 업데이트 빈도수를 높이면 안됩니다.

 

 클라이언트는 서버에서 동작중인 tick의 비율로 마우스나 키보드 등의 입력장치를 검사하여 user commands를 만듭니다. User commands는 기본적으로 현재(!) 상태의 키보드와 마우스의 상태를 검사합니다. 그러나 각 user commands가 담겨있는 패킷을 서버에게 보내는 대신, 초당 일정한 개수(보통 30개)의 user commands 패킷을 서버에게 보냅니다. 이것은 동일한 패킷이 두 번 이상 보내질 수 있다는 것을 의미합니다.

 

 게임 데이터는 네트워크 부하를 줄이기 위해 delta compression을 사용하여 압축되어 집니다. 이것은 서버가 패킷을 보낼 때 마다 전체 게임에 대한 정보를 보내지 않고, 마지막으로 보낸 패킷과 비교하여 변경된 부분만 보낸다는 것을 의미합니다. 서버와 클라이언트 사이에 보내지는 패킷은 넘버링이 되어지는데, 압축되지 않은 전체의 게임정보는 게임이 처음 시작되었거나 수 초 동안의 패킷 손실이 발생했을 경우에만 보내게 됩니다.

 

 유저의 입력이 실제 게임에 적용되기까지 걸리는 시간에는 많은 요인들이 작용합니다. 서버/클라이언트의 CPU 부하, 서버에서의 시뮬레이션 tick, 게임정보 업데이트 빈도수 등이 있지만 가장 크게 작용하는 것은 네트워크의 환경입니다.

 

Entity Interpolation

 
클라이언트는 기본적으로 초당 20 snapshot을 받습니다. 서버로부터 받는 정보만으로 게임상의 애니메이션, 이동 등을 진행시킨다면 뚝뚝 끊기는 등의 부자연스러운 상황이 나올 것 입니다. 또한 패킷 손실은 게임의 진행을 더욱 부자연스럽게 합니다. 이 문제를 해결하기 위해 가장 최근에 받은 두 snapshot 렌더링이 시작되는 시간으로 되돌아 가봅니다. 이 기술은 ‘client side entity interpolation’이라고 불립니다. 초당 20 snapshot은 각 snapshot간격이 50 ms 입니다. 클라이언트의 렌더링 하는 시간을 50ms 늦춘다면 가장 최근에 받은 snapshot과 그보다 한 단계 전 snapshot 사이의 상황을 렌더링 하게  됩니다. Source Engine은 100ms 의 딜레이를 갖습니다. 이 방법은 하나의 패킷이 손실되더라도 렌더링 시점 전후에 각 snapshot이 항상 있도록 해 줍니다. 아래 그림은 이러한 상황을 나타냅니다.

사용자 삽입 이미지

 마지막에 받은 snapshot은 10.30 입니다. 클라이언트 시간은 새로운 프레임이 렌더링 될 때는 현재 클라이언트 시간은 10.32 이고 렌더링은 이보다 0.1 초 이전인 10.22가 됩니다.

 

 100ms의 딜레이를 갖기 때문에 342 패킷이 손실되더라도 렌더링 하는데 있어 문제가 없습니다. 왜냐하면 340 패킷과 344 패킷을 가지고 렌더링할 수 있으니까요.

 

Input Prediction

 
100ms의 지연을 갖는 유저가 ‘이동’을 시작한다고 가정해 봅니다. ‘이동’ 키가 눌려지고 이 정보를 서버로 보냅니다. 서버는 이 패킷을 처리하고 클라이언트에게 snapshot을 보내게 되고, 클라이언트는 snapshot을 받은 다음에야 게임상에서 실제 ‘이동’을 하게 됩니다. 따라서 유저가 ‘이동’키를 누른 후 실제 게임에서 ‘이동’은 적어도 100ms 이후에야 이루어집니다.

 
유저의 입력이 이처럼 실제 게임에서 곧바로 나타나지 않는 것은 게임의 질을 떨어뜨립니다. 클라이언트에서의 ‘Input prediction’은 네트웍의 지연으로 인해 생기는 느린 반응성을 없애줍니다. 클라이언트는 오직 자신의 입력만을 예측합니다. 서버의 snapshot이 도착하기 전까지 예측된 입력으로 케릭터를 이동 시킵니다.

 
100 ms 가 지난 후, 클라이언트는 서버로부터 snapshot을 받습니다. 그리고 이전에 예측한 값과 서버로부터 받은 데이터를 비교합니다.

 

Lag Compensation

 
유저가 10.5 초에 목표를 향해 총을 쐈다고 가정합니다. 총을 쐈다는 정보는 네트워크를 거쳐 서버로 갑니다. 서버가 시뮬레이션을 하는 동안 목표는 움직여서 다른 위치에 있을 수 있습니다. 유저가 총을 쐈다는 정보가 담긴 패킷이 서버에 도착한 시간이 10.6초 라면  유저가 10.5 초에 실제로 목표를 적중시켰다 하더라도 서버에서는 적중하지 못했다는 결과를 낳게 됩니다. 이와 같은 문제점은 네트워크의 렉으로 인해 발생됩니다.
 
 ‘lag compensation system’은 일정 시간(기본적으로 1초) 동안의 플레이어 위치 정보를 기록하고 있습니다. 서버는 패킷이 도착한 시간이 아니라, 클라이언트에서 명령이 생성된 시간을 가지고 계산을 합니다. 클라이언트의 명령 실행시간(Command Execution Time)은 다음과 같은 공식으로 계산되어 집니다.

 
Command Execution Time = Current Server Time - Packet Round-Trip-Time - Client View Interpolation
 
 이렇게 얻어진 클라이언트의 명령 실행시간을 가지고, 서버는 그 실행시간의 클라이언트들의 상태로 되돌아갑니다. 그렇다면 유저가 쏜 총알이 정확하게 적중 됩니다. 그러나 서버에서는 entity interpolation도 함께 적용되기 때문에 예상치 못한 결과가 나타날 수도 있습니다. 유저의 명령이 실행되고 난 후에, 플레이어를 다시 원래 있던 곳으로 되돌아 갑니다. 서버상에서는
sv_showimpacts 1
을 입력함으로써 서버와 클라이언트의 서로 다른 hitbox를 확인할 수 있습니다.

사용자 삽입 이미지

 위의 스크린샷은 200 밀리세컨드의 렉이 있는 서버환경에서의 타겟(=다른유저)의 모습입니다. 빨간색 그림은 클라이언트에서 100 밀리세컨드 이전의 타겟 위치를 나타냅니다. 패킷이 서버에 도착하는 시간 동안에도 타겟은 계속 왼쪽으로 이동합니다. 패킷이 서버에 도착하면 서버는 패킷이 생성된 시간을 바탕으로 타겟의 위치를 파란색 그림으로 복원합니다. 서버는 총의 발사를 추적하고 명중을 확인합니다. 클라이언트와 서버의 명중계산은 정확히 일치하지 않습니다. 왜냐하면 시간을 측정하는데 있어 아주 조금의 오차가 있기 때문입니다. 수천 분의 일초라도 빠르게 이동하는 물체에게는 몇 인치만큼의 오차를 발생시킬 수 있습니다. 멀티플레이어의 명중은 완벽하지 않고, 그것은 tickrate와 움직이는 물체의 속도에 따른 한계가 존재합니다. Tickrate를 높이는 것은 명중계산의 정밀도를 높이지만, CPU, 메모리, 서버와 클라이언트의 네트웍 대역폭을 더욱 필요로 합니다.

여기서 한 가지 궁금증이 생기는데 그것은 ‘서버에서의 명중계산은 왜 이렇게 복잡한가?’ 입니다. 플레이어의 위치와 명중확인을 클라이언트에서 계산하는 것은 쉽고, 픽셀만큼의 정밀도를 가질 수 있습니다. 플레이어가 명중되면 단지 서버에게 ‘명중되었음’이라는 메시지를 보내면 됩니다. 그러나 이렇게 할 수는 없습니다. 왜냐하면 이것은 중요한 판정인데 서버는 클라이언트를 신뢰할 수 없기 때문입니다. 심지어 클라이언트가 깨끗하고 Valve-Anti-Cheat으로 보호된다 하더라도, 서버로 가는 패킷은 제 3자에 의해 변조될 수 있기 때문입니다.

네트웍 지연과 랙은 실제와 일치하지 않는 패러독스를 만듭니다. 예를 들어, 당신은 누구로부터 명중 당했지만, 당신을 그를 볼 수 없는 경우도 생깁니다. 이런 경우는 서버에서의 계산이 위의 그림에서 파란그림 위치로 계산되었기 때문입니다. 이러한 불일치 되는 문제는 일반적으로 해결할 수 없습니다. 왜냐하면 빛보다 느린 패킷의 속도 때문입니다. 실제 세게에서는 이러한 문제를 볼 수 없는데, 그것은 우리 모두가 똑 같은 세상을 볼 만큼 빛이 충분이 빠르기 때문입니다.

 

Net graph

소스엔진은 클라이언트와의 연결 속도, 그리고 품질 등을 체크할 수 있는 몇 가지 툴을 제공합니다. 가장 인기 있는 것은 net graph입니다. 들어오는 패킷은 오른쪽에서 왼쪽으로  움직이는 작은 선으로 표시됩니다. 세로의 높이는 패킷의 크기를 나타냅니다. 선이 끊어진다면 그것은 패킷을 잃어버렸거나 순서가 틀린 경우입니다. 선은 패킷의 내용에 따라 다른색을 띕니다.

 

첫 번째 선은 초당 렌더링된 프레임 수, 평균 지연시간, cl_updaterate 등을 보여줍니다. 두 번째 선은 마지막 패킷의 크기, 들어오는 대역폭의 평균, 초당 받은 패킷 수를 보여줍니다. 세 번째 선은 나가는 패킷을 나타냅니다.

 

사용자 삽입 이미지

Optimizations

초기 네트웍 세팅은 서버를 통하여 플레이할 수 있도록 세팅되어 있습니다. 이 세팅은 대부분의 클라이언트/서버의 하드웨어와 네트웍 설정에서 잘 동작하도록 되어 있습니다. 인터넷 게임을 위해서는 클라이언트의 “rate”항목만을 조정해 주면 됩니다. 이 “rate”항목은 네트웍환경의 가능한 대역폭을 정의하는데 모뎀에서는 4500, ISDN에서는 6000, DSL에서는 10000이상의 값을 추천합니다.

 

'Others' 카테고리의 다른 글

Bad Pointers  (0) 2008.10.14
Suspend(), Terminate() 함수를 쓰면 안되는 이유  (0) 2008.08.02
소스엔진의 멀티플레이를 위한 네트워킹  (0) 2008.06.28
Posted by tvhead

댓글을 달아 주세요

티스토리 툴바