AWS Lambda Web Functions에서 1993년 게임 Doom을 실행하는 데모가 공개됐다. 게임은 서버에서 WebAssembly로 구동하고, DynamoDB에 입력 기록을 저장해 요청이 다른 실행 환경에 도착해도 게임을 이어간다.

데모에서 게임을 체험할 수 있으며, 저장소에서 배포할 수 있다. 이 구현은 요청이 여러 실행 환경으로 분산되고 세션 고정이 없는 환경에서 상태가 있는 장시간 연결을 처리하는 방법을 보여준다.

게임 엔진으로는 새 플랫폼에 이식할 수 있도록 만든 Doom 엔진 버전인 doomgeneric을, 게임 데이터로는 무료 오픈 게임 데이터인 Freedoom을 사용했다. 엔진은 GPL, Freedoom은 BSD 라이선스여서 전체를 공유할 수 있다고 설명한다.

Lambda Web Functions는 Node.js 24를 실행하며 Node.js는 WebAssembly를 지원한다. C로 작성된 엔진을 zig cc로 WASI 대상 2.7MB WebAssembly 모듈로 컴파일하고 node:wasi로 불러왔다. 함수가 직접 실행하는 것은 JavaScript이며, 게임 엔진은 .wasm 패키지처럼 의존성으로 로드된다.

doomgeneric에 필요한 화면·키보드·시계를 제공하는 플랫폼 파일도 구현했다. Doom은 기존의 8비트 320x200 화면 버퍼에 그림을 그리고, Node.js가 WebAssembly 메모리에서 픽셀을 읽는다. 키 입력은 Node.js가 게임 입력 큐에 넣는다. 시스템 시계 대신 Node.js가 시간을 설정해 게임을 한 틱(1/35초)씩 진행한다. 시계를 호스트가 관리하기 때문에 같은 틱에 같은 입력을 주면 픽셀 단위로 같은 게임 상태가 나온다. 환경 간 인계와 장애 복구, 관전자 기능은 이 결정성에 기대고 있다. 게임 하나는 틱마다 CPU를 약 0.15ms 사용한다.

브라우저에는 초당 17.5프레임을 하나의 긴 HTTP 응답으로 스트리밍하고, 키 입력은 작은 POST 요청으로 보낸다. Web Functions에서 WebSocket을 사용할 수 없기 때문이다. 문서가 스트리밍 방식으로 제안하는 서버 전송 이벤트(SSE)는 텍스트만 지원해 프레임을 base64로 바꾸면 크기가 3분의 1 늘어난다. 따라서 구현에서는 바이너리 응답을 스트리밍한다.

프레임은 이전 프레임 이후 바뀐 부분만 전송한다. 가만히 서 있을 때는 전송량이 거의 없지만, 회전하면 모든 픽셀이 바뀌어 초기 구현에서 플레이어당 초당 약 890KB를 사용했다. 게임 속도는 유지하면서 프레임을 하나씩 건너뛰도록 해 약 500KB/s로 줄였다. zstd와 brotli도 시험했으며 전송량을 추가로 30~40% 줄일 수 있었지만, 모든 브라우저가 이를 기본 지원하지 않아 적용하지 않았다.

게임 상태와 실행 환경 인계

요청이 어느 실행 환경으로 갈지 고정할 수 없고, 환경이 예고 없이 중단될 수도 있다. 전체 게임 상태를 매 틱 저장하는 방식은 WebAssembly 메모리 상태가 수 MB라 초당 35회 기록하기에 적절하지 않다. 구현에서는 게임 상태 대신 입력을 저장한다. 엔진이 결정적이므로 입력 기록을 재생하면 같은 상태를 다시 만들 수 있다는 설명이다.

DynamoDB 테이블에는 게임별로 실행 환경 소유자와 하트비트를 기록하는 임대 정보, 소유자가 적용한 키 입력 기록, 게임을 실행하지 않는 환경에 도착한 키 입력을 담는 대기함이 저장된다. 요청이 게임을 실행하지 않는 환경에 도착하면 그 환경이 게임을 요청한다. 기존 소유자는 마지막 입력을 기록하고 DynamoDB 트랜잭션 하나로 게임을 넘긴다. 새 소유자는 입력 기록을 재생해 같은 틱부터 이어간다. 재생에는 플레이 1분당 약 30ms가 걸렸으며, 테스트에서 새 소유자의 첫 프레임은 기존 소유자의 마지막 프레임과 픽셀 단위로 같았다.

소유 환경이 인계 없이 종료되면 하트비트가 2.5초 지난 뒤 다른 환경이 게임을 맡는다. 이 경우 입력은 최대 0.5초까지 손실될 수 있다. 게임 아래 상태 표시줄은 실행 환경 이동과 복구에 걸린 시간을 알려준다. 저장소에는 테이블 구조와 타이밍, API가 포함돼 있다.

상태 표시줄에는 프레임 속도, 대역폭, 요청을 처리하는 실행 환경, 리전, 브라우저에서 함수까지의 왕복 시간, 접속한 CloudFront 엣지도 표시한다. 관전자 링크와 QR 코드를 제공해 다른 사람이 게임을 조작하지 않고 볼 수 있다. 관전자 요청이 게임 실행 환경에 도착하면 같은 프레임을 받고, 다른 환경에 도착하면 입력 기록을 재생해 게임을 복제한 뒤 새 입력을 따라간다. 복제된 게임은 약 1초 뒤처지며, 테스트에서는 플레이어 화면과 프레임이 일치했다.

배포 시에는 이벤트 기반 Lambda 런타임과 달리 Web Function 런타임에 AWS SDK가 포함되지 않으므로 @aws-sdk/client-dynamodb를 패키지에 넣어야 한다. CDK에는 현재 Web Functions용 L1 구성 요소만 있어 IAM 역할과 코드 업로드를 직접 연결해야 하며, L2 구성 요소는 개발 중이다. 사용자에게 제공할 때는 사용자 지정 도메인을 CloudFront에 연결했다. 원본 액세스 제어로 요청에 서명하고 엔드포인트에 IAM 인증을 설정해 CloudFront를 우회할 수 없게 했으며, 배포 뒤 함수의 리소스 정책을 설정하는 스크립트를 사용했다.

이 글은 멀티플레이어 액션 게임처럼 모든 플레이어가 하나의 빠른 게임 루프를 공유하는 경우에는 매치마다 Lambda MicroVM을 적용할 수 있다고 제안한다. Web Functions는 입력 기록으로 상태를 재구성할 수 있는 사용자별 상태에도 맞을 수 있으며, 문서 편집기나 워크플로 실행을 예로 든다. 저장소는 cdk deploy 한 번으로 배포할 수 있다.