만들리에/오락실 (스트리밍 게임)

추억의 오락실 (3/11) - 초반 리서치

gamz 2021. 1. 12. 23:23

스케치

일단 러프하게 어떻게 구현할 수 있을지 스케치를 해본다.

 

 

대충 통밥으로 게임 에뮬레이터, 비디오/오디오 인코딩, 스트리밍 및 컨트롤이 있을 것 같다. 이들이 어떻게 조화를 이룰지 좀더 살을 붙여보자면,

1. 서버에서 돌고 있는 게임 에뮬레이터는 자신의 tick 에 맞춰 열심히 프레임(이미지, 오디오)를 만들어 낼텐데

2. 이 프레임을 넘겨받아 코덱을 이용해 인코딩을 한 후 

3. 연결된 클라이언트(브라우저)에 인코딩된 패킷을 전송(스트리밍)하면

4. 클라이언트(브라우저)는 video element를 이용해 동영상 데이터를 플레이한다

5. 유저의 키입력이 발생하면 이걸 서버로 보내고

6. 서버에서는 이 키입력을 게임 에뮬레이터로 전달해 게임에 반영한다

7. 그리고는 1번으로 가서 계속 반복

 

게임 에뮬레이터

찾다보니 게임 에뮬레이터 오픈소스가 이미 꽤 많다. Atari, NES, Gameboy 등 특정 머신 에뮬레이터도 있고 MAME 처럼 여러 에물레이터를 모두 지원하는 것들도 있다. MAME 하면 레트로 에뮬레이터로 널리 알려져있기도 하고 커버하는 타이틀도 많아 오락실 에뮬레이터로 가장 적절하다고 봤다.

 

MAME는 1997년부터 개발되어 오다가 2016년 5월에 오픈소스 라이센스를 달고 정식으로 오픈했고 현재도 굉장히 활발히 진행되고 있는 오픈소스 프로젝트다.

 

현재 MAME 오픈소스 그대로 빌드하면 바로 게임을 실행할 수 있는 콘솔앱이 나오는데 이를 UI는 없이(headless) 라이브러리 형태로 만들어야할 필요가 있었다. 게임 실행을 컨트롤하고 유저 입력을 넘겨주고 비디오/오디오 프레임을 직접 뽑아내는 등의 인터페이스가 필요하다.

 

MAME 는 메인 코드는 C++로 구현되어 있으며 MacOS/Windows/Linux 환경에서 빌드를 모두 지원한다. 내 관심은 Linux 환경이고 그 중에서도 Debian/Ubuntu 정도면 무난할 것 같아 이 빌드 가이드에서부터 시작을 했다.

 

비디오/오디오 인코딩

MAME에서 게임이 실행되며 만들어지는 이미지(RGBA)와 오디오(PCM) 데이터를 그대로 클라이언트에 전달해볼 생각도 했었다. 하지만 대역폭이나 데이터전송 비용을 생각했을때 압축(인코딩)을 안할 수는 없어보였다.

 

영상 통화가 비디오보다는 오디오에 더 민감한 것과는 다르게 게임 스트리밍은 오디오보다 비디오에 더 민감하다. 하지만 비디오 품질을 높이려고 하면 인코딩 latency가 늘어나게되어 정상적인 frame-rate 의 플레이가 어려워지는 등의 아슬아슬한 줄타기 이슈들이 떠오른다. 또한 중간에 키프레임(key-frame)이 잘못되기라도 하면 다음 키프레임까지는 정상적인 렌더링이 어렵다. 이는 일반 동영상 시청이었다면 별 이슈가 안되겠지만 게임 플레이에서는 승패를 가르거나 한 목숨이 날라갈만한 중요한 이슈가 된다.

 

이 비디오 인코딩 파라미터 튜닝 관련은 이런 포스트를 참고하면서 해봤다. 파라미터별로 정확한 의미/의도를 마스터하진 못했지만 이런 부분이 있다는걸 알게된 계기였다. 코덱 자체는 대부분의 웹브라우저가 지원하는 H.264(비디오), OPUS(오디오)를 사용하기로 했다.

 

스트리밍 및 컨트롤

클라이언트는 일단 브라우저(모바일 브라우저 포함)라고 봤다. 그렇다면 일단 웹표준을 따르는 방식으로 접근해야하는데 다행히도 이미 좋은 프로토콜이 있다. 실시간을 지향하며 동영상 뿐만 아니라 일반 데이터도 주고 받을 수 있는 채널을 가지고 있고 모바일 브라우저에도 지원하고 있는 그 프로토콜 WebRTC 말이다.

 

WebRTC를 활용하는 유스케이스는 대부분 클라이언트-클라이언트로 통신을 하지만 오락실은 서버-클라이언트(브라우저)로 통신을 하게 되겠다. 서버2클라이언트의 단방향 비디오/오디오 채널과 클라이언트2서버의 단방향 데이터 채널을 열어서 각각 스트리밍 데이터와 유저 컨트롤 입력 데이터 전송에 사용하도록 한다.

 

한편, 반대로 WebRTC를 안쓰고 구현할 수 있는 방법이 있나도 생각해봤다. 일단 유저 컨트롤 입력 데이터는 간단하다. 웹소켓이든 그냥 HTTP 요청이든 서버로 보내면 되니까. 스트리밍이 문제인데, 일단 라이브 스트리밍에 많이 사용하는 RTMP를 웹에서는 직접 사용할 수는 없기 때문에 HTTP 기반으로 동작하는 대표적인 스트리밍인 HLS 혹은 MPEG-DASH(표준)을 써서 스트리밍을 해야하겠다. 하지만 이것들은 특정 시간 혹은 프레임 단위로 쪼개서(chunk) 그것들을 전송하게 되는데 이미 첫번째 청크를 쪼개기까지 버퍼링이 필요하기에 실제 게임 상태와 유저가 보는 영상간의 랙(lag)이 계속 존재하게 되는 이슈가 있다. 이걸 아무리 줄여보려고 하겠지만 쉽지는 않아보인다.

 

또 다른 방법으로는 인코딩을 아예 하지 않은 프레임 데이터(바이너리)를 웹소켓을 이용해 클라이언트에 보내고 클라이언트는 그 프레임 데이터를 캔버스(canvas element)에 그리면 되지 않을까? 실제로 초반 프로토타이핑에서 실험해 보았고 잘 동작하긴 한다. 그러나 해상도나 프레임 수를 올리게 되면 데이터 전송량이 너무 많아지고(비용) 픽셀 단위로 캔버스에 그려야하는 등 CPU 소모가 커진다. (인코딩을 했을때는 하드웨어 코덱의 도움도 받을 수 있고 만약 모바일이라면 오히려 CPU만 쓰는 것 보다 배터리 소모도 더 작을 것으로 예상) 그래서 이 방법을 목표로 하긴 어려웠다.

 

기술 스택

사이드 프로젝트를 하는데 있어 나에게는 어쩌면 프로젝트 내용보다도 이것이 더 중요한 것 같기도 하다. 아무래도 회사에서는 많이 사람들이 함께 일하고 유지보수 관점도 생각해야하기 때문에 좀더 발산하지 않는 방향으로 접근하게 되다보니, 이런 사이드프로젝트는 생업으로는 거리가 먼 분야의 일이라던지 새로운 언어 혹은 프레임워크의 사용, 실험적인 방법론 등을 적용해보고 스킬을 연마할 수 있는 좋은 수단이 된다.

 

처음엔 그냥 익숙한대로 파이썬으로 할까 하다가 언제부턴가 앞으로는 타입언어만 쓰겠다고 마음먹은게 떠올라서 생각을 틀었다. 에뮬레이터 부분은 C++로 빌드된 MAME 라이브러리의 바인딩을 잘 지원해주는 언어였으면 했는데 마침 당시 레이더에 들어와 있던 Rust 가 공부도 할겸 딱이었다. 웹서버 및 백엔드 레이어는 조금 더 익숙한 Golang 을 쓰기로 했다. 프론트는 무난하게 많이들 쓰는 Typescript + Next.js 를, 운영환경은 k8s로 결정했다.