0. 1인 플레이
네트워크 플레이가 되는 레이싱 게임을 만들어봅시다. 뭐부터 해야하죠?
일단은 1인 플레이가 되야 겠지요. 코스 위에 캐릭터가 올바르게 서 있고, 가속키를 누르면 가속하고, 브레이크 키를 누르면 감속하고, 커브도 좀 틀 줄 알고...
뭐 이런거야 왠만한 게임들과 다를 바가 없죠.
1. 기본적인 네트워크 플레이
자, 이제 2인 이상의 네트워크 플레이가 되도록 해봅시다.
서로 간의 통신이 되야겠군요. 유명한 프로토콜로 TCP와 UDP가 있네요.
어떤 걸 사용해야 하죠? 뭐, 서로의 차이점은 구글님께 물어보면 금방 나오니깐 자세히 설명은 하지 않겠습니다. 여튼 보아하니 UDP는 TCP보다 일반적으로 빠릅니다. 해야할 일을 좀 적게 하니깐요. 하지만 그렇기 때문에 확신이 없습니다. 이걸 어떻게 활용할까요?
저라면 아래와 같이 설계하겠습니다.
'나 가속키 누르고 있어!'
'나 지금은 브레이크키 안 누르고 있어!'
그 특정 클라이언트는 이렇게 수집된 정보를 이용해서 각 클라이언트의 상태를 계산합니다. 이런 역할을 하는 녀석을 슈퍼 피어(super peer) 혹은 로컬 서버(local server) 등의 이름으로 보통 칭하더군요. 여기서는 이후 '슈퍼 피어'라고 칭하겠습니다.
슈퍼 피어는 상태값을 계산해서 다시 각 클라이언트들에게 브로드캐스팅해줍니다. 역시나 UDP를 사용하고요.
'지금 A 플레이어는 요기쯤 있어연.'
'B 플레이어는 점프중이네연.'
이러한 정보는 중간에 1~2개쯤 잃어버려도 괜찮답니다. 어차피 순식간에 다음 패킷이 와서 최신 상태를 갱신해주니깐요. 그렇기 때문에 빠르게 많이 보낼 수 있는게 좋습니다. 조금이라도 최신 정보인 편이 좋으니깐요. (이 부분이 본 포스팅의 주제인 동시에, 뒤에서 좀 더 자세히 설명하게 됩니다.) 그래서 UDP를 사용합니다.
순서가 바뀌는 것도 사실은 곤란하기 때문에 각 패킷에는 시간 정보를 첨부합니다.
하지만 '아이템을 먹었다! 사용했다!' 이런 류의 정보라면 UDP로 보내기엔 좀 곤란합니다.
내가 부스터 아이템을 먹어서 사용했는데 아무런 반응이 없어봐요. 얼마나 억울하겠어요? 그래서 이 경우엔 신뢰할 수 있는 TCP 프로토콜을 이용해서 서버에게 정보를 알립니다. 그럼 서버는 이 녀석이 정말 아이템을 가지고 있나? 등의 검증을 거친 후 모두에게 알려주게 되지요.
2. 상태 동기화
여기까지 제작이 되었다면 어디 한 번 플레이 해보도록 합시다.
얼랄라~ 옆에 가는 놈들 움직임이 튀어요!! 당연한 이야기지요. 슈퍼 피어가 UDP로 최신 상태를 계속 보내준다고는 해도 얼마나 자주 보내주겠습니까? 상태 정보의 구조체를 아무리 간소히 한다고 해도 아래 정도는 되지 않을까요?
이미 40바이트인데요. 8명이 함께 하고 있다면 320바이트네요. 초당 10번쯤 보낸다고 해도 결국 10프레임짜리 움직임이 되니깐 자연스러워 보일리가 없지요. 그럼 어쩌지요?
자, 제 목표는 30프레임이라고 해봅시다. 그 정도면 적당히 자연스러운 움직임으로 보일 것 같습니다. 그렇다면 초당 30번, 정보를 받아 그리는 방법이 가장 아름답겠습니다만, 네트워크 사정이 허락을 해줄 것 같지 않습니다. 어쩔 수 없이 초당 10번만 보내기로 하고... 정보를 받는 사이에 2프레임씩만 더 그려주면 30프레임이 되지 않나요?
이를 위해 각 클라이언트는 외삽을 합니다. 즉, 1번 프레임의 데이터를 받으면 그걸 이용해서 슈퍼 피어와 같은 방식으로 계산해서 2번 프레임의 데이터를 얻습니다. 2번 프레임의 데이터를 받기 전에 미리 미래의 데이터를 구해두는 거지요. (각 클라이언트는 당연히 슈퍼 피어가 될 수 있으므로 그 계산 루틴을 가지고 있습니다.)
그리고 1번 프레임을 그렸던 시간과 2번 프레임을 그리게 될 시간, 그리고 현재의 시간을 이용해서 현재 프레임의 데이터를 얻습니다. 간단한 선형 보간이지요.
참~ 쉽죠잉~~~!
물론, 이 와중에 2번 프레임의 데이터가 오면 다시 덮어주고 다음 프레임을 외삽하고 사이 데이터들을 보간해서 구합니다. 이 때 2번 프레임 값으로 바로 그려버리면 외삽으로 구한 추측값과 달라서 위치나 모션이 살짝 튈 수도 있습니다. (혹은 2번 프레임 데이터는 잃어버리고 3번 프레임 데이터가 올 수도 있습니다.) 그런 경우라면 그 데이터를 그리는 데에 바로 사용하지 않고 외삽/보간 값을 구하는데만 활용해도 좋습니다.
이런 방식을 데드 레커닝(Dead Reckoning)이라고 하더군요.
실제로 구현을 해보면 외삽을 위해 위에서 언급한 데이터에 추가로 데이터가 더 필요하게 될 겁니다. 그렇다보면 UDP로 주고 받을 데이터량이 너무 많아지는 건 아닌가 싶지만 그 경우엔 변경된 데이터만 보내주는 방식으로 좀 줄일 수가 있습니다. 물론 변경된 데이터의 종류를 표현하기 위한 비트 플래그 따위가 있어야 겠고요.
선형 보간으로 자연스러운 움직임이 나오지 않을 정도로 네트워크 사정이 안 좋다면, 클라이언트의 CPU 타임을 좀 더 사용하는 방법도 있습니다. 슈퍼 피어가 과거의 프레임 3개를 보내거나, 2개를 보내고 클라이언트가 미래의 프레임 2개를 외삽해서 베지어 곡선을 그리듯이 보간을 하는 거죠. (...라곤해도 전 이렇게 구현해본 적이 없어서 어떨지 모르겠습니다.)
3. 그 밖의 이슈
이것만으로도 가지고 놀만 하지만 서비스하기에는 완전치 못합니다. 특히 우리 나라와 같은 공유기 환경에서는 UDP 홀펀칭이 반드시 필요합니다. 그리고 보안 이슈도 있습니다. 누군가 독한 맘 먹고 클라이언트를 해킹해서 슈퍼 피어를 조작하면 게임 전체가 조작되어 버릴 수 있습니다. (간간히 주요 정보를 서버에 TCP로 보내서 검증하는 보완책이 있겠지요.)
또 슈퍼 피어의 연결이 끊어졌을 때도 문제가 발생합니다. 빠르게 그 역할을 대신해줄 클라이언트를 정해줘야 겠지요. 뭐, 일반적인 P2P 게임에서는 방장이 나가면 걍 게임이 종료되어 버리는 식으로 구현한 경우도 많아서, 유저들도 이 경우는 인정해주는 눈치기는 합니다.
* 사설
생뚱맞게 왜 이런 포스팅이 갑자기 올라왔냐고 묻는다면 그냥 스트레스 해소라고 답변하겠습니다. 길게 적으면 구차해질뿐...


댓글을 달아 주세요
보통은... 이런글 쓰고 있으면 스트레스가 쌓이지 않나요?
-_-;;;
혹시 천성이 개발자?
ㅎㅎ. 예전에 했던게 생각이 안 나는게 매우 스트레스더군요.
이렇게라도 정리해두는 습관이 필요할듯!
여기 웨 이래!
1. 왠지 오픈아이디 인증이 실패로, 오픈 아이디 글쓰기가 안되는군.
2. 잡다한 나 카테고리 누르면 에러 나.
정보 제공 ㄳㄳ.
업그라운드 하고 캐시 지우니깐 되는군? 다시 해보아요~
1.오픈아이디 된다.
2.잡다한 나 카테고리 오류 안남.
무려 보름만에 적는 확인글이군. (....)
님도 업데이트 좀 하자.
추신 : 면접 어케 댐? 딴덴 좀 알아보고 있음?
아. 나 먹고 살 길이 막막해.
확인 ㄱㄳ.
면접은 오늘 야그했다시피 아직 소식이 없군?
딴데 알아봐야하나... 일단은 좀 더 기다려볼라고!
님도 찾아보면 많을텐데!!
쉬고 있는 동안 1년치 포스팅이 올라왔근!
ㅋㅋㅋ.
횽은 이제 블로깅 안 함?