[▲ 배현직 대표]

인벤에서는 넷텐션의 배현직 대표님이 GDC2017에 참석, '오버워치' 강연을 들은 후기를 공유해드립니다. 배현직 대표님은 인벤을 통해 지난 2월부터 6월까지 게임서버와 관련해 6부작 칼럼을 기고하신 바 있습니다.

국내최초로 서버엔진을 개발하고 상용화한 '넷텐션'은 '프라우드넷(ProudNet)'이라는 이름의 서버 엔진을 개발한 게임 서버&네트워크 엔진 개발사입니다.

'프라우드넷'은 세븐나이츠,클로저스,스트리트파이터5 등 200개 이상의 개발 프로젝트에서 사용되고 있습니다.

※ 넷텐션 공식홈페이지 바로가기


작년에 이어 올해도 미국의 게임 개발자 컨퍼런스(GDC)에 또 오게 되었습니다. 다들 아시다시피 오버워치가 대박 인기가 났죠.

청중들을 가득 메운 자리에서 그들은 오버워치의 게임 플레이가 어떤 구조로 작동하는지, 그리고 멀티플레이 프로그램 즉 넷코드가 어떻게 작동하는지를 과감히 소개하는 강연을 열었습니다.


미리 말씀드릴 것이 있습니다. 강연 내용 자체가 멀티플레이 개발에 경험과 지식이 이미 있다는 전제로 전개된 강연이었습니다. 그래서 저는 여기서는 그들의 강연 내용만 적지 않고, 이 강연을 이해하는데 필요한 배경 지식도 같이 설명하겠습니다.

모든 한국인이 그렇듯이 저는 영어 듣기가 약합니다. 그래서 원래 강연 내용보다 깊이가 부족할 수 있습니다. 이점 미리 양해를 부탁할게요. 저는 웬만한 장르의 온라인 게임의 멀티플레이 프로그래밍에 오래 경험을 쌓아 왔습니다. 뭐, 엄청 틀리지는 않겠죠.


출처: 웃긴대학

일반적인 FPS 게임은 총, 수류탄,연막탄과 몇 가지 특이한 것들 등 그렇게 많지 않은 무기류 패턴을 갖고 있습니다. 그러나 오버워치는 다릅니다. 오버워치나 다른 하이퍼 FPS 류 게임들은 게임 기획자 마음대로 무기의 패턴을 정할 수 있습니다. 그렇다 보니 오버워치는 다른 FPS 게임과 다른 부분에서 개발이 까다로운 데가 있습니다.

무기의 패턴이 다양하고, 게다가 캐릭터의 행동 패턴도 유별난 것들이 많습니다. 트레이서의 시간 역행처럼 극단적인 스킬들도 있습니다. 이러한 것들이 뒤섞이는 게임에서는 여러 가지 위험 요소가 도사립니다.

첫 번째로 버그 문제입니다. 오버워치는 캐릭터와 무기가 무척 다양한 게임입니다. 그러하다 보니 무기나 캐릭터의 행동이 주는 영향의 패턴도 너무나도 다양합니다.

트레이서1이 점멸 스킬로 이동하는 경로에다가 정크랫이 덫을 놨습니다. 트레이서1이 덫에 걸립니다. 다른 트레이서2가 트레이서1 폭탄을 붙입니다. 트레이서1은 시간 역행을 합니다.
그러면 폭탄은 바닥에 떨어지고 트레이서도 점멸하기 전 어딘가로 가겠죠. 이것이 우리가 아는 내용입니다.

가상의 상황을 꺼내봅시다. 어느 날, 사람들의 밸런스 제보가 많이 들어옵니다. 오버워치 개발진은 이를 해결하려고 트레이서의 점멸 스킬을 버프 시켜줍니다. 정크랫 덫을 통과하게 해주는 거죠. 그런데 여기서 버그가 생깁니다. 시간 역행을 하면서 덫 있던 위치 전으로 후퇴하는데, 그 와중에서 덫에 걸려버립니다.

이런 일이 발생하면 우리는 그걸 뭐라고 하죠? “버그다!”

블리자드의 능력자들은 이런 버그도 하나 못 고치냐고 말할 수도 있겠죠. 프라이드가 강한 블리자드 사람들은 이런 말을 듣고 싶지 않을 것입니다.

그런데 난항이 또 있습니다.

여러 사람이 멀티플레이로 게임을 하는데, 레이턴시(한 기기에서 저쪽 기기로 데이터가 가는데 걸리는 최소 시간)는 어쩔 수 없이 생깁니다. 레이턴시 자체는 근본적으로 없앨 수 없습니다. 여러분의 게임기로부터 서버까지 연결된 네트워크 망 자체를 업그레이드한다면 모를까요. 도로 밑에 땅 파고 난리도 아니겠죠.

나라면 가능할지도...

결국 이 레이턴시는 어쩔 수 없이 있다는 전제하에, 어떻게 이 레이턴시를 감출까를 고민해야 합니다. 이를 레이턴시 마스킹(latency masking)이라고 부릅니다. 오버워치처럼 캐릭터의 행동이 다들 제각각 독특한 경우에는 이 레이턴시 마스킹을 만드는 데 드는 추가적인 노력이 많이 들어갑니다. 안타깝게도 레이턴시 마스킹은 한 가지 방법만이 있지 않습니다. 캐릭터의 행동 패턴이 다양할수록 레이턴시 마스킹 테크닉도 그만큼 늘어나게 됩니다.

재밌는 게임을 만들기 위해서는 개발 과정에서 만들고 수정하고 버리는 과정을 무척이나 많이 해야 합니다. 블리자드가 게임 하나 출시하는데 무척 오래 걸리지만 블리자드표라는 말이 나올 만큼 게임 재미성이 보장되기로도 유명하죠. 그 뒤에는 어떤 비밀이 있는지는 모르지만, 제 생각에는 아마도 엄청나게 많이 만들고 버리는(!) 과정을 하고 있기 때문이 아닐까 싶습니다.

“에잇, 맘에 안들어! 쨍그랑!” (출처: 티스토리 블로그)

요약하자면 오버워치는… (아마도) 많이 만들고 버릴 수 있는 자체적인 개발 속도를 갖추면서도 레이턴시 문제 앞에서 최대한 해결책을 만들어나갈 수 있는 것이 필요했습니다. 오버워치의 게임 시스템은 캐릭터,무기, 행동 패턴을 기상천외한 것을 쉽게 만들어 넣을 수 있게 하는 것을 목적으로 하고 있어 보였습니다.

그러면 어떻게 할까요? 이것을 쉽게 만들 수 있는 시스템을 만드는 것입니다. 바로 ECS(Entity-Component-System) 구조입니다. 아, 이거 좀 판이 커지겠구나 싶더니만, 역시나 강연이 끝날 때쯤에는 이런 생각이 들더군요.

“보트를 만들기 힘들어서 항공모함을 만들었다.”

출처: 티스토리 블로그

...아, 그들이 비경제적이라는 뜻이 아닙니다. 항공모함을 만들면 그 위에다가 비행기를 얹어서 날릴 수 있잖아요? 현재 고생으로 미래를 편하게 하자는 것이죠. 오버워치의 캐릭터와 무기가 무척 재미있는 이유가 다 있는거죠.

ECS 구조는 이렇게 생겼습니다.


ECS 구조를 쉽게 설명해보고자, 바로 예를 들어보겠습니다.

여기 상자가 있습니다. 이 상자는 그냥 아무것도 하지 않고 가만히 있습니다. 이 상자를 엔티티(Entity)라고 부릅니다. ECS의 첫글자죠.


이 상자는 게임 플레이에 아무런 영향을 끼치지도 않습니다. 가령 플레이어가 이 상자에 부딪히면 그냥 뚫고 지나갈 뿐입니다.


우리는 이 상자에 생명력을 불어넣고 싶습니다. 그래서 우리는 이 상자에게 ‘부딪힐 수 있게 해라’를 부여합니다. ‘부딪힐 수 있다’라는 컴포넌트(Component)를 붙입니다. ECS의 두 번째 글자죠.


그리고 이 상자에게 우리는 추가적인 능력을 부여합시다. 뭔가에 부딪히면 색을 바꿉니다. 이것을 위해서 이 상자는 ‘자기의 색깔’이라는 정보를 따로 가질 수 있어야 합니다. 이것도 컴포넌트입니다.


그런데 ‘부딪힐 수 있다’ ‘자신의 색깔을 가질 수 있다’ 등은 그 자체로 바로 이루어지지 않습니다. 누군가가 그게 될 수 있도록 해주는 프로그램이 필요합니다.

색깔을 가지거나 바꾸는 것은 남에게 영향을 안 줍니다. 하지만 뭔가와 부딪힌다는 것은 둘 이상의 엔티티가 관여되어야 할 일입니다. 하나 이상의 엔티티에 대해서 이러한 일을 하도록 만들어진 프로그램을 시스템이라고 부릅니다. ECS의 S죠.


그렇습니다. 게임 플레이를 하는 동안 전장 안에서는 각 플레이어들은 엔티티입니다. 트레이서가 던진 펄스 폭탄도 엔티티고, 정크랫의 덫도 엔티티입니다. 로드호그가 던지는 갈고리도 엔티티입니다.

정크랫의 덫은 ‘뭔가가 닿으면 붙잡아라’라는 컴포넌트가 있고 ‘뭔가를 닿으면’ ‘붙잡게 하는 무언가’ 등을 주관하는 배경의 무언의 손길을 시스템이라고 부릅니다.

시스템의 수는 수십 가지가 넘었습니다. 캐릭터와 무기, 행동을 자유롭게 조합할 수 있게 하기 위해서 많은 수의 개발 도구를 중무장시킨 것이죠. 돌려 말하면 오버워치 개발에 참여한 사람들은 이 수십 개의 서브시스템을 달달 외우고 활용할 수 있어야 하겠죠.

게임에 등장하는 캐릭터, 무기,아이템 등이 다양하니만큼 이들 간의 상호작용은 무척 복잡해집니다. 이 과정이 마구잡이로 조합되다보면 생각지도 못한 버그가 튀어날 수 있습니다.

그래서 오버워치를 프로그래밍할 때 시스템에만 최대한 코드가 모이게 합니다.

그리고 내부적으로 업무 규칙을 두어야 했다고 합니다. 가령 어떤 시스템이 하는 행동이 부작용을 일으켜야 할 때는 부작용을 일으키는 곳이 쉽게 발견될 수 있게 하자는 것입니다. 다른 시스템의 영향을 받는 쪽에서 프로그래밍 하지 말고 영향을 주는 쪽에서 프로그래밍을 하자는 것입니다.

이러한 방법은 일부 프로그래머들에게는 받아들이기 힘든 규칙일 수도 있습니다. 하지만 당장에는 불편하더라도 명시적으로 원인을 찾기 힘든 것보다는 낫다 쪽에 손을 들어주고 있습니다.

그렇습니다. 프로그래밍이 빠른 사람은 프로그램을 만드는 속도가 빠른 것이 아니라, 버그를 잡는 속도가 빠르거나 버그 자체를 적게 냅니다. 경험이 많은 프로그래머일수록 이 말을 절감하게 됩니다.

오버워치는 서버 위주의 클라이언트-서버 방식을 하고 있습니다.


아, 이것이 뭔지 설명해야겠네요. 저는 멀티플레이 방식을 크게 세 가지로 나누어봅니다.

1) 서버 위주의 클라이언트-서버 방식: 게임 플레이 로직 처리를 서버가 다 하는 방식입니다. 클라이언트는 서버에게 키 입력 정보만을 보내줍니다. 서버에서는 모든 캐릭터의 행동에 대한 연산을 합니다. 가령 걸어가게 한다던지, 총알을 맞았을 때 해야 하는 행동을 만든다던지 등의 연산을 다 합니다.



2) 클라이언트 위주의 클라이언트-서버 방식: 게임 플레이 로직의 일부를 클라이언트가 분담하는 방식입니다. 클라이언트는 자기가 조종하는 플레이어 캐릭터에 대한 연산을 해서 서버에게 그 연산 결과를 보내줍니다. 서버에서는 NPC나 수류탄 등 일부 캐릭터에 대해서만 연산을 합니다.



3)P2P 방식: 게임 플레이 전부를 클라이언트가 합니다. 클라이언트는 자기 플레이어에서 한 행동 전부를 서버에게 보냅니다. 서버는 이를 받아서 다른 클라이언트들에게 중재합니다. 서버가 필요 없기 때문에 클라이언트들끼리 직접 정보를 주고받습니다.



위에서 1(서버 위주의 클라이언트-서버 방식)은 서양권의 패키지 게임이 오랫동안 발전하면서 다져진 방식입니다. 이 방식은 플레이어가 해킹하기 어렵다는 장점이 있습니다.

하지만 서버에서 처리해야 하는 연산량이 많아져서 서버의 유지비용이 크게 증가하는 단점이 있습니다. 패키지 게임에서는 유저들이 직접 서버를 띄우기 때문에 상관없지만, 오버워치처럼 유저가 직접 서버를 띄우는 형태가 아닌 경우에는 얘기가 달라집니다. (오버워치의 경우 서버에서 이 연산량을 줄이기 위해 구체 단위 충돌 감지 등의 트릭을 이용하였습니다.)

이 방식으로 만들어진 게임은 레이턴시가 조금만 길게 나와도 여러 문제가 튀어나옵니다. 내 캐릭터가 절뚝거린다던지 하는 것들이죠. 그래서 이 방식은 고속 유선 네트워크를 쓰는 PC나 콘솔 게임에 적합합니다. 무선 랜으로 플레이할 때는 신호 강도가 높게 잡히는 곳에서, 그리고 주변에 무선랜을 같이 쓰는 사람들을 좀 쫓아낸 후에 해야 하겠죠.

2(클라이언트 위주의 클라이언트-서버 방식)은 한국과 중국의 온라인 게임이 발전하면서 다져진 방식입니다. 이 방식은 플레이어가 해킹하기 쉽다는 단점이 있습니다. 클라이언트-서버 방식이 해킹에 강한 경우는 위에 1 방식뿐입니다.

하지만 이 방식은 위 1보다 훨씬 적은 수의 서버가 있어도 되며, 중국이나 동남아시아처럼 인터넷 환경이 약한 곳에서도 매끄러운 게임 플레이가 된다는 장점이 있습니다.

해킹을 줄이기 위해서 플레이어의 이동 정보를 서버가 약간의 유효성 검사를 한다던지, 해킹 되면 곤란한 것들만 골라내서 위 1의 방식을 한다던지 합니다. 이렇게 해서 일부 해킹을 줄일 수 있습니다.

3(P2P 방식)은 슈퍼 피어 방식과 대칭 피어 방식으로 또 구별됩니다. 슈퍼 피어 방식은 위 1의 서버가 실제로는 클라이언트 중 하나가 대신하는 방식입니다. 대칭 피어 방식은 위 2의 방식과 비슷하지만 각자의 플레이어 캐릭터의 이동 정보를 서버 대신 다른 클라이언트에게 직접 보내는 방식입니다.

P2P 방식은 서버가 멀리 떨어져 있어도 쾌적하게 멀티플레이를 할 수 있게 해줍니다. 그러나 서버가 게임 플레이에 관여하지 않는지라 해킹에 약합니다. 그래서 게임 플레이를 할 때 중간 정산 결과를 서로에게 보내주어서 일이 중간 정산이 안 맞으면 해커라고 간주한다던지, 일부 해킹되면 안 되는 처리만 서버에서 하거나 합니다. 이렇게 해서 일부 해킹을 줄일 수 있습니다.

오버워치는 1의 방식(서버 위주의 클라이언트-서버 방식)을 하고 있기 때문에 레이턴시에 민감한 게임입니다. 특히 위도우메이커의 헤드샷은 더 민감할 수밖에 없죠.

제 짐작상, 오버워치에서 이 문제를 해결하기 위해 전 세계에 광범위한 영역에 클라우드 서버를 대량 흩뿌려 놓지 않았을까요? 아마도 이렇게 양으로 밀어붙여 해결했을지도 모릅니다.

클라이언트와 서버 간 네트워킹에서는 레이턴시도 있지만 패킷 유실이라는 것도 있습니다. 패킷 유실이라 함은 보낸 데이터가 상대에게 못 가고 버려지는 것을 말합니다. 많은 경우 패킷 유실이 발생하면 레이턴시가 그만큼 커짐을 의미합니다.

인터넷에서 레이턴시와 패킷 유실을 없앨 수 없습니다. 여러분이 워낙 부자라서 집부터 서버 사이에 게임 전용 케이블을 추가로 깐다면 모를까요.

아 글쎄 나라면 가능할 것 같다니까...

오버워치는 레이턴시나 패킷 유실에 의한 보기 싫은 현상을 줄이기 위해서 레이턴시 마스킹을 다양하게 사용했습니다. 어디 봅시다.

오버워치에서는 게임 플레이의 진행 결과를 두 가지로 나누어서 전개합니다. 보이는 것과 안 보이는 것이죠.

여러분의 에이밍과 키 입력은 서버에서 연산됩니다. 여러분이 눈에 보이는 적은 서버에서 연산한 것이 온 것이기 때문에 레이턴시만큼 이미 과거 위치가 됩니다. 헤드샷 같은 일격필살 상황에서는 이 시간의 오차가 억울한 랙사로도 이어집니다.

그래서 오버워치에서 화면에 보이는 것은 레이턴시를 측정해서 나온 만큼의 약간의 미래(?)의 모습입니다. 그리고 내부적으로는 현재 시점의 모습을 갖고 있게 합니다. 연산은 그것으로 이루어지는 것이죠. 어찌 보면 눈속임 같다는 생각이 드시겠지만, 이렇게라도 하지 않으면 레이턴시 앞에서 답답한 게임 플레이가 일어나게 됩니다. 한조는 더욱 심각한 한조충이 되겠지요.

패킷 유실이 발생하게 되면 현재-미래 차이가 문제가 아니라 아예 오류 상황이 되어버립니다. 이것을 막기 위해서 오버워치는 입력 정보 패킷을 두 번씩 보냅니다. 제가 알기로는 이 방식은 대전 격투 게임 등에서 흔히 쓰는 방식인데 오버워치는 이를 FPS 게임에 응용했네요. 이 방식은 별거 아닌 것 같아도 꽤 유용합니다. 일반적으로 인터넷 품질이 많이 나빠도 유선 네트워크에서는 패킷 유실률이 20%를 넘지 않습니다. 무선 네트워크는 넘기도 하지만요. 그러다 보니 두 번 보내는 것이 모두 실패할 가능성은 거의 없습니다.

한편 클라이언트에서는 서버에게 키 입력을 보냈지만 클라이언트는 클라이언트 나름대로 게임 플레이 연산을 합니다. 다만 서버로부터 결과가 오게 되면 이를 유지하지 않고, 측정된 레이턴시만큼 시간을 과거로 돌리고 다시 연산해서 현재 상태로 복구합니다. 이 과정에서 내분점 계산은 필수적으로 하고요. 이들 과정 중 하나라도 빠지게 되면 여러분은 게임을 하다가 심한 어지러움을 느끼게 될 것입니다.


패킷 유실이 발생하게 되면 두 번씩 보내는 것에 의해 다음번에서 상황이 회복됩니다. 하지만 그만큼 이미 시간은 지났으므로 다시 시간을 뒤로 돌려서 재계산을 해줍니다.

시간을 과거로 돌려서 다시 연산하는 것은 자기 플레이어 캐릭터에 대해서만 하고, 다른 플레이어 캐릭터에 대해서는 그냥 멈춰버리게 하는 꼼수도 취하고 있습니다. 그래서 랙이 생기면 다른 플레이어의 움직임이 멈춰버리는 이유가 여기에 있습니다. 하지만 워프는 잘 안 하죠. 내분점 연산을 해주기 때문입니다.

오버워치는 시간을 과거로 돌리는 것을 여러 가지 용도로 쓰고 있는데요, 이 레이턴시 마스킹 기법뿐만 아니라 킬캠, 최고의 플레이(POTG) 보여주기 등에서 활용하고 있습니다. 아, 트레이서의 시간 역행도 있겠네요.

결론적으로 모든 플레이어는 트레이서의 시간 역행 스킬을 알게 모르게 자주, 항상 쓰고 있다고 볼 수 있습니다. 물론 그 시간이 워낙 짧아서 여러분은 못 느끼겠지만요.

오버워치는 많은 곳에서 물리엔진을 쓰고 있습니다. 그러하다 보니 1/30초 혹은 1/60초의 일정 시간마다의 연산을 필요로 합니다. 그것도 한치 오차 없이 말이죠. 제 경험상도 물리엔진을 쓸 때 총알이 벽을 뚫지 못하게 하려면 이렇게 시간 진행을 고정시켜주어야 합니다.

하지만 플레이어는 제각각 다른 하드웨어를 쓰고 있고 화면에 얼마나 많은 캐릭터들이 나와줄지 예상할 수도 없기 때문에 이것을 고정시키는 것은 까다로운 문제입니다. 그럼에도 불구하고 까라면 까는 블리자드 개발자들은 이 정도쯤이야!입니다. 하하하.

후아… 이쯤 되니, 레이턴시 마스킹의 구조가 체계화될 수도 없게 됩니다. 캐릭터의 종류와 행동 패턴이 악랄한 기획자 머리에서 마구 튀어나오는 마당이네요.

이렇게 복잡해지는 것을 어떻게 오버워치 제작자들이 감당할 수 있을까요? 그 답은 Statescript에 있습니다.

Statescript는 오버워치 개발진이 자체적으로 개발한 시스템입니다. 게임 개발을 마치 레고 블록 조립하듯이 재미있게 (그리고 다시 만들어!라는 압박 스트레스를 받으면서) 해주는 도구입니다.

블리자드의 멋쟁이 프로그래머들은 기획자들이 머리 싸매야 하는 이 복잡한 문제들을 Statescript라는 레고 블록 도구를 만들어 도루 기획자들에게 퉁 던져줍니다. 그리고 그들의 까다로운 주문을 그들이 직접 감당하게 합니다. 물론 프로그래머들은 보트 대신 항공모함을 만드느라 노력 깨나 했겠습니다만.

이 Statescript는 이런 식으로 작동합니다.

컴포넌트가 가져야 할 속성을 특수한 파일에 만들어 넣습니다. 그러면 이것을 컴파일해서 로직 에디터에서 보이게 합니다. 그리고 이 컴파일 결과물은 오버워치의 C++ 코드에서 다룰 수 있는 데이터 구조가 됩니다.

기획자는 로직 에디터에서 편집을 하고, 그동안 프로그래머는 이 데이터 구조들을 관리하고 제어할 시스템 프로그램을 만듭니다.


이렇게 만들어진 것은 게임을 테스트하면서 그 로직 도식이 어떻게 작동하는지를 실시간으로 확인합니다. 이렇게 게임 화면 위에 오버레이 되어서 말이죠.


이렇게 함으로서 업무가 분명하게 분화됩니다. 기획자는 오버워치의 각 캐릭터들이 할 행동에 대해서 직접 레고 블록 조립하듯이 만들고, 프로그래머는 레고 블록으로 만으로는 만들기 힘든 시스템들을 계속해서 추가해 넣습니다.

사실 많은 게임회사들은 이렇게 게임을 만듭니다. 언리얼 엔진의 블루프린트가 이러한 방향을 지향하고 있고 이것은 언리얼 엔진의 개발 파이프라인을 용이하게 해주고 있습니다.


오버워치의 ECS 구조는 유니티에서 먼저 발견할 수 있습니다. 유니티의 엔티티-컴포넌트 관계 개념은 게임 개발에서 획기적인 유연성을 가져다주었습니다. 이것 때문에 유니티의 개발이 단순해지고 재미있을 수 있게 되었습니다.


우연인지는 모르겠지만 오버워치는 이 두 엔진의 특징을 어느 정도 닮았습니다.

그들의 이 노력은 효과적이었다고 합니다. 업무 분업뿐만 아니라 개발 후반으로 가면서 모두가 일이 쉬워졌습니다. 개발 프로젝트 중 초반에는 새 캐릭터를 추가할 때마다 프로그래머가 만들어야 하는 시스템의 수가 많았습니다.

발표 자료에서 보여준 바로는 트레이서가 가장 많았습니다. 그리고 개발 후반으로 가면서 줄어들고 아예 없어지는 경우도 있었습니다. 가령 디바의 경우 하나도 없더군요. 최근에 추가된 솜브라의 경우 꽤 특이한 스킬들을 가진 캐릭터인데도 불구하고 추가된 시스템의 수는 몇 개 없었습니다.

네트워크 성능 문제를 같이 고민하면서도 자유롭고 빠르게 게임을 개발해 나갈 수 있도록 노력했던 오버워치 개발팀의 노력을 보여준 강연 후 모두가 박수를 쳐줬습니다.

지금까지 오버워치의 아키텍처와 넷코드가 어떻게 작동하는지 대략 살펴봤습니다. 재미있게 읽으셨나요?