[멀티플레이어 게임 프로그래밍] 1장 네트워크 게임의 개요
네트워크 게임의 개요
멀티플레이어 게임의 간추린 역사
로컬 멀티플레이어 게임
가장 초창기의 멀티플레이어 게임은 한 대의 PC에 둘 이상의 플레이어가 함께 즐길 수 있도록 고안된 형태이다. 아마 피카츄 배구? 이는 본격적인 멀티 프로그래밍보단 싱글 플레이 게임과 크게 다르지 않았다.
초기 네트워크 멀티플레이어 게임
1970년대에 이르러 PC, 즉 개인용 PC가 보급되면서 직렬 포트로 컴퓨터를 연결하는 방식이 등장했다. 이를 통해 게임 세션을 구성한 초창기 네트워크 PC 게임이 등장했다.
MUD: 멀티 유저 던전
머드라고 불리는 멀티 유저 던전형 게임은 텍스트 기반으로 같은 가상 공간에 여러명이 접속하여 즐기는 형태의 게임이다. PC 성능이 강화되면서 제조사가 모뎀을 제공하기 시작했다. 당시에 이를 활용하면 매우 느리긴 하지만 멀리 떨어진 컴퓨터와 통신이 가능했다.
랜 게임
근거리 통신망(Local Area Network, LAN), LAN은 상대적으로 가까운 지역 내에서 서로 연결된 컴퓨터의 네트워크를 말한다. 앞서 말한 직렬 포트를 직접 연결한 경우도 해당한다. 이후 이더넷이 확산되면서 비로소 전성기를 맞게 된다.
랜 멀티플레이를 선보인 몇몇 게임이 있었지만, 둠
이야 말로 현대 네트워크 게임의 선구자이다. 1993년에 개발된 둠은 최대 4명까지 접속할 수 있었으며 같이 협동 모드를 즐기거나 대결 모드로 즐길 수 있었다.
온라인 게임
대형 통신망을 통해 지리적으로 멀리 떨어진 컴퓨터끼리 연결하여 플레이하는 것을 온라인 게임이라고 한다. 인터넷이 90년대 폭발적으로 보급되면서 온라인 게임도 시대의 요구에 부응하기 시작했다. 이때 등장한 에픽 게임즈의 언리얼이 대표적이다.
온라인 게임과 랜 게임의 구현 원리가 거의 같다 싶기도 하지만 온라인 게임으로 가면서 가장 큰 골칫거리로 떠오른 건 바로 레이턴시(Latency), 즉 네트워크로 데이터를 전송하면서 발생하는 시간 지연이다. (시대가 지나면서 개선되고, 방법들이 많이 나왔다.)
MMO 게임
오늘날에도 대다수 멀티플레이어 게임이 접속 인원수를 제한하는데, 그 숫자는 대부분 4인 내지 32인이다. 그에 반해 MMO(Massively Multiplayer Online) 게임은 하나의 게임 세션에 수백 명, 수천, 수만의 플레이어가 동시에 참여할 수 있다. 이것이 대개 MMO가 RPG인 까닭이기도 하다.
모바일 네트워크 게임
모바일 지형으로 게임의 영역이 확장되면서 모바일에도 멀티플레이어 게임이 등잘한다. 모바일 플랫폼에서 멀티플레이어 게임은 대개 비동기식으로 구현된다. 비동기식 멀티플레이 모델에선 데이터를 실시간으로 전송할 필요가 없다.
하지만 오늘날에는 모바일 네트워크 자체가 발전하여 실시간 네트워크를 지원하는 게임들이 속속 등장하고 있다. (하스스톤)
<스타워즈: 트라이브스>
1998년에 출시된 SF장르의 FPS게임인 스타워즈: 트라이브스는 멀티플레이어 게임의 역사에 큰 발자취를 남겼다. 이 게임은 당시 128명까지 접속이 가능했고 랜이나 인터넷으로 플레이 가능했다. 당시 전화 접속용 모뎀으로 대부분 접속하였고 속도가 56.6kbps였다. 이로 인해 레이턴시가 높아 게임이 끊기거나 렉이 걸리는 경우가 많았다.
이러한 각박한 상황속에서 네트워크 모델을 설계한 게임을 다루는 이유는 현대의 네트워크 모델에서도 상당 부분 적용할 수 있기 때문이다.
네트워크 게임을 설계할 때 가장 먼저 정해둘 것은 통신 프로토콜이다. 통신 프로토콜이란, 두 대의 컴퓨터 사이에 어떤 데이터가 오고 갈지를 정해둔 규약이다. 트라이브스는 비신뢰성 프로토콜인 UDP를 사용했다. 즉, 네트워크로 보낸 데이터가 수신자에게 반드시 도착한다는 보장이 없다는 뜻이다.
그렇지만 게임에 참여한 모든 플레이어에게 중요한 정보까지도 모조리 비신뢰성 프로토콜로 보내면 문제가 야기될 수 있다. 따라서 개발할 때 데이터의 종류에 따라 어떻게 보낼지를 구분해 두어야 한다.
- 전달 미보장 데이터: 말 그대로 게임에 있어 그다지 중요하지 않은 데이터를 지칭한다. 대역폭이 고갈되면 게임 시스템은 이런 종류의 데이터부터 생략해 버린다.
- 전달 보장 데이터: 수신이 보장되어야 하며 나아가 데이터가 보낸 순서대로 도착하는 것도 보장되어야 하는 데이터이다. 게임에 있어 매우 중요하다고 판단되는 데이터로, ‘플레이어가 총을 발사했다.’는 이벤트가 그 예이다.
- 최신 상태 데이터: 최신 상태가 아니면 전달할 의미가 없는 성격의 데이터이다. 예를 들면 특정 플레이어의 체력 수치가 그렇다. 지금 현재 HP를 알고 있다면 5초 전의 HP가 얼마였는지는 굳이 전달할 필요가 없다.
- 특급 전달 보장 데이터: 최우선으로 보내야 하며 아울러 전달이 보장되어야 하는 데이터가 여기에 속한다. 플레이어 위치 정보가 그 예로, 시간이 지체될수록 정보의 가치가 급격히 떨어지므로 최대한 빨리 전달해야 한다.
트라이브스가 사용한 네트워킹 모델에선 이들 네 가지 데이터 성격에 따라 여러 세부사항을 결정했다. 또 한 가지 중요한 결정사항은 바로 피어-투-피어(pear-to-pear, P2P) 대신 클라이언트-서버 모델(client-server, 이하 CS 모델)을 채택한 것이다.
CS 모델에서는 모든 플레이어가 중앙 서버 하나에 접속하는 데 반해, P2P 모델에선 각각의 플레이어가 모든 플레이어와 연결을 유지해야 한다. P2P모델은 O(n^2)의 대역폭이 필요하다. 이는 사용자 수의 제곱에 비례하여 대역폭이 소모된다는 뜻이다.
트라이브스는 n 즉, 사용자 수가 128명이라면 n의 제곱인 128^2 = 16384의 대역폭이 필요하다. 이 같은 문제를 피하고자 CS 모델을 채택했다. 클라이언트-서버로 구성하면 각 플레이어에 할당되는 대역은 상수로 고정되며 서버에서만 O(n)의 대역폭이 소모된다.
트라이브스의 네트워킹 모델은 스택의 형태를 가지고 있었다.
플랫폼 패킷 모듈
패킷(packet)이란 네트워크로 보내기 위해 데이터를 묶어 놓은 한 단위를 말한다. 트라이브스 모델의 최하위 계층은 플랫폼 패킷 모듈이다. 이 모듈은 여러 계층 중 유일하게 플랫폼 종속적인 계층이기도 하다. 이 계층은 본질적으로 표준 소켓 API를 래핑 즉, 감싸둔 것에 불과한데, 다양한 패킷 형식으로 조립하고 전송하려는 목적으로 래핑한 것이다.
트라이브스는 비신뢰성 프로토콜을 사용하므로, 전달이 보장되어야 하는 데이터 처리를 위해 몇 가지 메커니즘을 추가할 필요가 있다. 신뢰성 계층을 직접 구현하기로 했으며 이를 상위 계층의 고스트 관리자, 이동 관리자, 이벤트 관리자가 신뢰성 관련된 처리를 나누어 담당한다.
연결 관리자
연결 관리자의 역할은 두 컴퓨터 사이의 연결을 추상화하는 것이다. 윗단의 스트림 관리자가 내려주는 데이터를 받아 아랫단인 플랫폼 패킷 모듈로 전달한다. 연결 관리자 수준에서도 여전히 신뢰성을 보장하지 않는다. 데이터를 책임지고 전달해 주지는 않는다는 것이다. 대신 연결 관리자 DSN(Delivery Status Notification)을 보장한다.
쉽게 맡긴 패킷이 전달되었는지 여부까지만 연결 관리자가 확실히 알려준다는 뜻이다. 이러한 상태 통지를 확인하면 상위 계층 관리자는 특정 데이터가 무사히 전달되었는지 판단할 수 있다. 배달 상태 통지는 수신 측의 확인 응답에 따라 비트 필드를 이용한 슬라이딩 윈도우 기법으로 구현된다.
스트림 관리자
스트림 관리자가 주로 하는 일은 다른 여러 상위 관리자를 대신하여 데이터를 연결 관리자에 보내는 것이다. 이때 중요한 처리는 바로 허용 최대 데이터 전송률을 조절하는 것이다. 전송률은 인터넷 품질에 좌우되며, 논문에서는 56.6kbps 모뎀을 쓰는 경우 초당 10패킷에 200바이트, 곱하면 대략 초당 2킬로바이트 정도로 패킷 전송률을 잡는 예를 들고 있다. 최대 전송 빈도와 크기는 서버에 접속할 때 클라이언트가 알려주는데, 서버가 데이터를 너무 많이 보내 과부하를 주지 않도록 하기 위함이다.
여러 시스템이 각자 스트림 관리자에 데이터 전송을 요청하므로, 이들 요청의 우선순위를 관리하는 것도 스트림 관리자의 역할이다. 대역폭이 제한된 상황에선 이동 관리자, 이벤트 관리자,고스트 관리자의 요청이 최우선으로 처리된다. 스트림 관리자는 어떤 데이터를 보낼지 결정한 다음 패킷을 꾸려 연결 관리자에게 내려보낸다. 이어서 스트림 관리자는 전송을 요청했던 상위 관리자들에게 각자의 데이터가 잘 전달되었는지를 알려준다.
전송 주기와 패킷 크기를 스트림 관리자가 결정하므로, 한 패킷에 여러 종류의 데이터를 섞어 보내는 경우가 다반사이다. 패킷 하나를 열었을 때 이동관리자의 데이터가 일부, 이벤트, 고스트 등 다른 데이터가 섞여 있을 수 있다.
이벤트 관리자
이벤트 관리자는 게임 시뮬레이션 중 발생하는 이벤트의 대기열을 관리한다. 이들 이벤트는 일종의 간이 RPC(Remote Procedure Call)로 RPC란 호출 시 원격 머신에서 실행되는 함수 또는 프로시저를 뜻한다.
예를 들어 플레이어가 총을 쏠 때 관련 시스템이 player_fired라는 이벤트를 이벤트 관리자에 보낸다. 그러면 관리자가 서버에 해당 이벤트를 보내는데, 서버는 이를 받아 검증한 후 실제 사격을 처리한다. 이벤트의 우선순위를 매기는 것은 이벤트 관리자의 권한으로, 가장 우선순위가 높은 이벤트부터 기록해 나가다가 특정 조건이 되면 처리를 중단한다.
구체적으로는 패킷이 꽉 차거나 이벤트 큐가 비었을 때 혹은 현재 계류 중인 이벤트가 너무 많은 경우 여기에 해당한다. 이벤트 관리자가 각 이벤트의 전송 기록을 추적하여 이벤트의 확실한 전달을 보장한다. 이때 전달을 보장하는 방법은 아주 간단하다. 보장하려는 이벤트의 확인응답이 없으면 대기열 맨 앞에 해당 이벤트를 다시 한번 끼워 보내면 된다.
고스트 관리자
고스트 관리자는 128인 멀티플레이를 실현하는 데 있어 가장 중요한 시스템이다. 상위 수준에서 고스트 관리자가 하는 일은 특정 클라이언트에게 유의미하다고 여겨지는 동적 객체를 복제 혹은 ‘고스트’사본을 만드는 것이다.
이는 클라이언트가 서버에서 받아둔 여러 객체의 정보를 일컬어 클라이언트상 서버 객체의 ‘고스트’라 칭하는데, 이 고스트를 전송 또는 수신하는 것이 고스트 관리자의 역할이다. 클라이언트에 객체 정보를 보낼 때 고스트 관리자는 그 클라이언트에 딱 필요한 정보만 걸러서 보낸다. 클라이언트가 반드시 파악하고 있어야 하는지, 알아야 하는지를 게임 시뮬레이션 계층이 책임지고 판단한다.
이에 따라 게임 객체에 고유한 우선순위가 부여되는데, ‘반드시 파악’해야 하는 객체는 높은 우선순위로, ‘알아 두어야’하는 정도라면 후순위로 부여된다. 어떤 객체가 클라이언트의 인지 범위에 포함되는지, 즉 스코프에 포함되는지 여부를 판정하는 데는 다양한 방법들이 있다.
어떤 방식으로든 유의미한 객체 집합을 일단 계산하고 난 다음에 고스트 관리자가 하는 일은 서버에서 클라이언트로 가능한 많은 객체 상태를 전송하는 것이다. 모든 클라이언트가 가장 최신의 상태로 업데이트되어 있게끔 보장하는 것은 고스트 매니저의 중요한 책무이다.
어떤 객체가 스코프에 포함되면(또는 연관성이 생기면), 고스트 관리자는 고스트 레코드라는 그럴싸한 이름의 부가 정보를 객체에 할당하는데, 이는 고유 ID, 상태 마스크, 우선순위, 상태 변경 여부 등 항목으로 구성된다. 고스트 레코드의 전송 순서는 일차로 객체의 상태가 변경된 것 먼저 그다음으로 레코드 자체의 우선순위를 만든다.
고스트 관리자 부분이 제일 이해하기 힘든 것 같다. 언리얼의 리플리케이션 시스템과 비슷한 것 같은데 다른느낌이다.
이동 관리자
이동 관리자의 역할은 플레이어의 이동 데이터를 최대한 빨리 전송하는 것이다. 멀티플레이어 게임에서 플레이어의 위치 정보는 매우 중요하다. 적 플레이어의 위치가 더디거나 늦게 전달되면 허공에 총알을 쏘는 셈이 되기 때문이다. 즉, 레이턴시를 줄여 플레이어가 지연을 느끼지 못할 정도로 이동 정보를 빠르게 전달해야 한다. 이를 위해 언리얼에선 예측 기법을 사용하기도 한다.
이동 관리자는 초당 30프레임의 빠른 속도로 입력 캡쳐를 수행하여 데이터를 생성하는데, 이 데이터에는 높은 우선순위가 부여된다. 1초에 30건씩 입력 정보가 쌓이므로 이 중에서 가장 최신의 정보를 빠르게 보내줘야 한다. 이동 데이터를 보내려면 스트림 관리자는 다른 것보다 가장 먼저 이동 데이터를 챙겨 내보낼 패킷 앞에 끼워 보낸다.
기타 시스템
트라이브스 모델에는 그 밖의 시스템도 몇몇 있지만 별로 중요하지 않다. 그 중에 데이터블록 관리자는 비교적 정적인 편에 속하는 게임 객체의 전송을 취급한다. 이와 구별하여 보다 동적인 객체는 고스트 관리자가 담당한다. 포탑이 바로 정적인 객체의 좋은 예로, 실제 이동하는 일은 없으므로 동적인 객체로 구분하지 않지만, 상호작용하여 상태 갱신이 일어나는 객체이다.
<에이지 오브="" 엠파이어="">에이지>
트라이브스와 비슷한 시기에 RTS(real-time strategy) 장르의 대표작인 에이지 오브 엠파이어가 등장했다. 비슷한 시기에 출시되어 마찬가지로 인터넷 전화 접속의 레이턴시 문제를 가지고 있었다. 에이지 오브 엠파이어는 결정론적 락스텝(deterministic lockstep) 모델을 채택했는데, 이 모델에선 컴퓨터 하나하나가 P2P 방식으로 다른 모든 컴퓨터와 연결하는 방식을 채용했다.
결정론이 보장되는 게임에선 모든 피어가 각각 동시에 병행하여 시뮬레이션을 진행한다. 그리고 게임이 진행되는 내내 모든 피어의 동기화를 맞추기 위해 통신에 락스텝을 사용한다. 트라이브스 사례와 유사하게 결정론적 락스텝 모델도 오랫동안 여러 게임에서 사용됐고, 최신 RTS에서도 여전히 쓰이는 기술이다.
RTS 멀티플레이어 게임을 네트워킹으로 구현하는 데 있어 FPS와 비교해 가장 큰 차이점은 플레이어의 가시권에 포함되는 유닛의 개수가 많다는 점이다. 앞서 다룬 트라이브스의 경우 128명을 지원하긴 하지만 특정 클라이언트 하나에 보여지는 플레이어 수는 비교적 적은 수에 불과하다. 에이지 오브 엠파이어의 경우 8인이긴 하지만 게임 시작 시점에는 50마리 이상의 유닛이 화면에 보여지기도 한다. 중후반엔 최대 200마리까지 늘어난다.
컬링을 사용해 가시권 판정을 계산할 수 있지만 최악의 경우를 생각해야 한다. 8명의 인원이 모든 유닛을 한 곳에 모아둔다면, 약 천마리가 넘는 유닛이 동시에 가시권에 들어오게 될 것이다. 유닛당 정보를 아무리 최소화하더라도 이렇게 많은 유닛을 동기화하기는 매우 어려운 일이다.
이 같은 문제를 해결하기 위해 개별 유닛 하나하나 동기화하지 않고 입력한 명령을 동기화하기로 결정했다. 작은 차이처럼 보이지만 이는 매우 중요한 설계상 결정이다. RTS에서 아무리 무리하더라도 분당 300회 이상 명령을 내리기 어렵기 때문에 대역폭 관리가 훨씬 수월하다.
그렇지만 유닛 정보를 네트워크로 보내지 않으므로 모든 플레이어의 게임 인스턴스는 시뮬레이션을 독자적으로 수행하므로 각 인스턴스를 다른 인스턴스와 정확히 동기화할 수 있는지가 극도로 중요해진다.
턴 타이머
각 게임 인스턴스마다 독립적으로 시뮬레이션을 수행하므로 P2P 형태의 토폴로지가 잘 어울린다. 서버가 중간에서 데이터를 중개할 필요가 없기 때문에 P2P모델에선 컴퓨터 사이에 데이터가 비교적 빠르게 오갈 수 있다는 장점이 있다.
하지만 각 플레이어가 각자의 정보를 서버 하나에 보내는 데 그치지 않고 다른 모든 플레이어에게 전송해야 하고, 모두 정확히 같은 시점에 이 명령을 처리해야 한다. 그렇지 않으면 각 인스턴스의 시뮬레이션이 그 시점부터 안 맞기 시작한다.
여기서 또 한 가지 중요한 점은 각 플레이어의 게임은 저마다 다른 프레임 레이트로 구동되고, 접속 환경도 품질도 차이가 날 수밖에 없다는 점이다. A플레이어가 명령을 내릴 때 곧바로 적용해 버리는 대신, 명령을 잠깐 대기시켜 둔 채로 일단 B, C, D에 보내어 모두가 준비되었을 때 비로소 동시에 적용하는 것이다. 하지만 여기에 또 문제가 있다. 만약 A가 명령을 내리지 않고 가만히 있는다면 게임이 매우 지척거리는 응답 속도를 보이게 될 것이다.
이에 대한 해결책이 바로 턴 타이머를 추가하여 일정 기간마다 명령을 쌓아두는 것이다. 턴 타이머 방식으로 구현하기 위해선 먼저 턴의 길이를 정해 두어야 한다. 턴의 기본 길이를 200밀리초로 잡고 이 200초 동안 모든 명령은 대기열 버퍼에 쌓아둔다. 200밀리초가 지나면 턴이 완료되어 그동안 대기열에 쌓아둔 그 플레이어의 모든 명령이 다른 플레이어에게 전송된다
핵심은 수신 측이 명려 대기열을 받는 즉시 처리하지 않고, 이후 두 번의 턴이 지난 다음에 처리한다는 것이다. 예를 들어 50번째 턴에 내려진 명령들을 인스턴스가 저마다 받아서 가지고 있다가 52번째 턴에 실행하는 식이다. 200밀리초짜리 타이머의 경우 인풋 랙, 즉 입력 후 화면에 반영하기까지 지연시간을 모두 합쳐 최대 200밀리초가 되는 셈이다.
명색이 실시간인 RTS 게임이 알고 보니 턴제로 구현된다는 점이 역설적일 수 있지만 턴 타이머 기법은 이미 스타크래프트 2에서도 적용된 기법이다.
턴 타이머 방식에서 고려할 마지노선은 한 명에게 심한 랙이 발생하여 200밀리초 타이머조차도 따라가지 못하는 경우엔 잠깐 중단하여 극복할 기회를 준다. 스타에서 많이 봤던.. 지속적으로 랙이 발생하여 게임에 지장을 준다면 내보내도록 처리할 수 있다.
이렇게 클라이언트가 입력한 명령을 모아 보내는 방식엔 또 한 가지 장점이 있다. 경기 진행 내내 처리된 모든 입력을 저장해 두더라도 메모리 용량이나 그 처리 부담이 적다는 것이다. 이를 통해 리플레이 기능을 쉽게 구현할 수 있다.
동기화
턴 타이머만으로는 각 피어 사이에 동기화를 확실하게 보장하기 어렵다. 각 머신이 명령을 받아 독립적으로 처리하므로, 이들 기기가 항상 같은 결과로 수렴토록 보장하는 장치가 절대적으로 중요하다. 대부분 게임은 행동의 결과에 약간의 임의성을 부여하는 경우가 많다. 쉽게 멀티 게임에서 궁수가 쏘는 화살의 랜덤성이 그러하다. 만약 A에서는 명중했지만 B에서는 빗나가는 경우가 발생한다면, 이는 동기화가 제대로 이루어지지 않았다는 뜻이며, 이후에 더 큰 문제를 일으킬 수 있다.
이 문제를 해결하기 위해 유사 난수 발생기(pseudo-random number generator)를 사용한다. 유사 난수 발생기는 시드(seed)라는 초기값을 입력받아 그 값에 따라 일정한 규칙에 따라 난수를 생성하는 알고리즘이다. 이를 통해 각 피어가 같은 시드를 사용하면 같은 결과를 얻을 수 있다. 흔히 맵 시드값, 동일한 랜덤 시드값을 사용하는 것이다.
동기화를 검사할 때 잠재적인 이점으로는 치트를 쓰기가 원천적으로 어려워진다는 것이다. 치트를 클라이언트에서 발생시키면 다른 클라이언트가 동기화문제로 바로 알아차릴 수 있다. 한편으로는 반대급부가 있는데, 동기화 시스템은 보여주어선 안 될 정보를 드러내 버리는 종류의 치트에는 무방비하다는 점이다. 소위 맵핵이라 불리는 것이 그 예이다.
느낀점
왜 그렇게 게임에 대한 설명이 많을까? 했는데, 서버의 트레이트 오프를 제대로 이해하기 위해선 게임적 특성을 제대로 봐야한다는 점을 잘 배운것 같다. 서버는 잘 돌아가는 척 하는 것이 아니라, 사용자가 느끼기에 최적화된 서버를 만들어야 한다. 게임은 사용자가 느끼는 것이 전부이기 때문이다.
1장은 대략적으로 앞으로 구현하게 될 내용들에 대한 역사와 정의들을 다루는 느낌이라 뒤에서 실제로 구현하게 될 내용들이 어떻게 적용되는지 궁금하다.
댓글남기기