원칙: 여러분이 실제로 할 게임을 찍는다

GameNow의 미리보기 영상은 게임 페이지에 붙는 영상이든, 그것으로 만든 세로 쇼츠든 모두 브라우저에서 진짜 게임을 플레이해서 찍습니다. 대부분은 four-ball.gamenow.us, pinball.gamenow.us처럼 각 게임 주소에 올라가 있는 운영 판을 그대로 엽니다. 목업이나 따로 렌더링한 트레일러, 편집으로 덧붙인 효과는 없습니다. 게임에서 실제로 찍을 수 없는 장면이라면 영상에도 나오지 않습니다.

사이트 미리보기는 하나하나 녹화 기록 파일에 남깁니다. 영상 길이, 영상과 포스터 이미지의 SHA-256 지문, 그리고 'tools/shorts/record.mjs로 녹화한 플레이 영상'이라는 자료 종류와 어떤 녹화 동작 파일로 찍었는지가 함께 적힙니다. 사이트는 이 지문의 앞 12자리를 영상 주소 뒤에 붙입니다. 그래서 제가 게임을 다시 녹화하면 브라우저에 남아 있던 예전 영상 대신 새 영상을 받게 됩니다.

이 원칙 때문에 버린 영상도 있습니다. 예전 물꼬 미리보기는 12초 동안 판만 비추고 아무 일도 일어나지 않았고, 예전 4구 당구 미리보기는 머리말까지 붙은 페이지 전체를 찍다가 겨누기만 하고 끝났습니다. 둘 다 버리고 실제로 판을 풀고 공을 치는 장면으로 다시 찍었습니다.

휴대폰인 척하는 헤드리스 브라우저

녹화 도구 record.mjs는 화면 없는 크로미움으로 게임을 엽니다. 쇼츠용으로 찍을 때는 안드로이드 휴대폰처럼 행동합니다. 화면은 CSS 픽셀로 432 × 672, 기기 배율은 2.5이고 터치를 켜 두므로 한 장면이 1080 × 1680으로 찍힙니다. 사이트 미리보기는 1280 × 720 데스크톱 창을 1.5배로 열어 1920 × 1080으로 만들고, 키보드와 마우스로 조작합니다.

녹화 전에 몇 가지를 바꾸는데, 녹화용 브라우저 안에서만 바꿉니다. 처음 방문 안내는 이미 본 것으로 처리합니다. 전체 화면 요청은 무시합니다. 녹화 도중 창 크기가 갑자기 바뀌면 영상이 찌그러지기 때문입니다. 포털의 메뉴 단추와 공유 단추는 게임 화면이 아니어서 숨깁니다. 숨기려면 콘텐츠 보안 정책(CSP)이 막는 스타일을 넣어야 해서, 녹화용 브라우저에서만 그 정책을 건너뜁니다.

정말로 플레이하는 스크립트

게임마다 녹화 동작 파일이 하나씩 있고, 두 부분으로 나뉩니다. setup은 카메라가 돌기 전에 메뉴를 넘기고 모드를 고르는 준비 단계이고, run은 실제로 녹화되는 15~25초입니다. 스크립트는 순위표에 아무것도 남기지 않는 연습·게스트 모드를 쓰고, 플레이어와 똑같은 입력으로 움직입니다. 화면을 누르고, 포인터를 끌고, 키를 누릅니다.

가장 손이 많이 간 게임은 4구 당구입니다. 이 게임의 샷 물리는 physics.ts 파일 하나에 들어 있고, 게임 자체와 AI 상대가 같은 파일을 씁니다. 녹화 도구는 바로 그 파일을 묶어 페이지에 넣고, 3D 장면에서 공 위치를 읽습니다. 제 차례가 오면 최대 약 550가지 샷을 시뮬레이션합니다. 빨간 공마다 두께 19가지에 힘 5단계, 네 쿠션을 이용한 빈쿠션 샷, 그리고 한 바퀴를 120방향으로 나눈 샷입니다. 득점하는 샷은 빨간 공을 하나만 맞히는 샷보다 훨씬 높게 치고, 반칙은 감점합니다. 점수가 높은 후보는 각도를 ±0.003·±0.006라디안, 힘을 게이지의 1.5%만큼 살짝 틀어 다시 쳐 봅니다. 그렇게 틀어도 득점하는 샷, 그리고 약하게 치는 샷을 고릅니다. 공이 오래 굴러 다음 차례까지 늘어지지 않게 하려는 것입니다. 고른 샷은 마지막으로 전체 시뮬레이션을 한 번 더 돌려 득점을 확인합니다.

치는 방법은 사람과 같습니다. 당구대를 끌어서 겨누고, Shift+← 또는 Shift+→로 게임의 0.05° 단위 미세 조준을 눌러 큐 각도를 맞춘 뒤, 오른쪽 힘 게이지를 고른 세기만큼 당겼다 놓습니다. 상대는 게임에 들어 있는 AI 보통이고, 목표 점수는 가장 큰 값으로 둡니다. 녹화 도중 경기가 끝나 결과 창이 뜨지 않게 하려는 것입니다.

NEON ORBIT 핀볼은 결정론적인 엔진으로 돌아갑니다. 1초에 물리 틱이 120번, 틱마다 세부 단계가 3번이고, 무작위로 일어나는 일은 모두 시드에서 나옵니다. 같은 시드에 같은 입력을 넣으면 같은 판이 나오고, 서버가 순위 기록을 다시 돌려 검증할 때도 이 성질을 씁니다. 미리보기를 찍을 때는 녹화용 브라우저가 페이지의 app.mjs를 불러오는 순간 고쳐서, 정해 둔 시드와 작은 플리퍼 봇을 끼워 넣습니다. 봇은 틱마다 입력값을 하나 돌려줍니다. 발사대를 120틱(1초) 동안 당겼다 놓고, 위로 튀어 오르는 중이 아닌 공이 플리퍼 바로 위 구역에 들어오면 그쪽 플리퍼를 올립니다. 같은 봇을 Node에서 시드 수천 개로 돌려 보고, 처음 30초 안에 범퍼 콤보·미션·멀티볼·잭팟이 모두 나오는 시드를 골랐습니다. 2026-10-08에 테이블 디자인을 바꿨을 때는 시드를 2827에서 430으로 바꿔 다시 찍었고, 영상 길이는 26.1초입니다.

다른 게임의 스크립트도 그 게임이 가진 데이터나 도구를 씁니다. 투비트 스크립트는 채보를 읽고, 귀에 들리는 곡 시각에 맞춰 F와 J를 누릅니다. 평균 오차가 0 근처가 되도록 8ms 일찍 누릅니다. 물꼬 영상은 물꼬 솔버가 낸 최소 풀이를 따라갑니다. 일부 스크립트는 일부러 박자를 들쭉날쭉하게 합니다. 일정한 박자로 두면 기계처럼 보였기 때문입니다. 미니골프는 처음엔 대충 당겨 보고, 고쳐 잡고, 잠깐 멈췄다 놓습니다. 물꼬는 같은 방향을 연달아 누를 때는 빨라지고 방향을 바꿀 때는 멈칫합니다. 그래도 미니골프에서 마지막으로 놓는 자리는 계산해 둔 정확한 샷 그대로입니다.

끊김 없는 선명한 화면

Playwright에 들어 있는 영상 녹화는 CSS 픽셀로만 찍어서, 휴대폰 화면이 432 × 672로 나와 흐릿했습니다. 그래서 크롬 개발자 도구 프로토콜(CDP)의 화면 전송(screencast)을 씁니다. 기기 해상도 그대로 품질 88의 JPEG 프레임을 받습니다.

screencast는 화면이 바뀔 때만 프레임을 보내서 시각이 들쭉날쭉합니다. 처음에는 ffmpeg의 concat 목록에 프레임마다 길이를 적어 이어 붙였습니다. 그런데 이 길이가 1/25초 단위로 반올림돼서, 모든 영상이 여섯 프레임마다 한 번씩 멈칫했다가 건너뛰었습니다. 움직이는 막대를 녹화해 보니 바로 보였습니다. 지금은 1/30초 칸마다 그 순간까지 도착한 마지막 프레임(반 칸 앞까지 허용)을 골라 번호 순서대로 늘어놓고, 30fps로 고정해 x264 CRF 18로 인코딩합니다.

무거운 3D 게임은 문제가 하나 더 있었습니다. 배율 2.5에서는 이 M1 맥이 초당 25장 정도밖에 그리지 못해서, 30fps 영상이 여전히 여섯 프레임마다 멈칫했습니다. 이런 게임은 동작 파일에서 기기 배율을 낮춰 찍고, 인코딩할 때 Lanczos 필터로 가로 1080까지 다시 키웁니다.

게임 소리를 그대로 녹음하기

소리는 페이지가 열리기 전에 Web Audio API를 살짝 고쳐서 받습니다. AudioContext마다 숨은 출력을 하나 더 만들고, 스피커로 가는 연결을 모두 그쪽에도 한 갈래씩 보냅니다. 녹화가 시작되면 출력마다 MediaRecorder로 250ms씩 Opus 소리를 녹음하고, 녹화 도중에 새로 생긴 AudioContext는 생긴 순간부터 녹음합니다. 포털 효과음과 게임 엔진이 소리를 따로 내는 경우가 많아서 필요한 처리입니다.

녹음한 갈래는 녹음 시작 시각과 첫 화면 프레임 시각의 차이만큼 밀거나 잘라 화면에 맞춘 뒤 섞습니다. 게임마다 소리 크기 차이도 컸습니다. 퍼즐 효과음은 -48dB 안팎, 리듬 게임은 -17dB 정도였습니다. 그래서 쇼츠는 -14 LUFS, 사이트 미리보기는 -18 LUFS로 맞춥니다.

컴퓨터가 한가할 때만 찍기

컴퓨터가 바쁘면 브라우저 게임이 끊기고, 그 끊김이 영상에 그대로 남습니다. 2026-10-05에 부하 평균 144인 상태에서 찍은 영상들은 쓸 수가 없었습니다. 그 뒤로 녹화 도구는 1분 부하 평균이 CPU 코어 수 아래로 내려갈 때까지 기다립니다. 녹화는 잠금 파일로 한 번에 하나씩만 돌고, 10분 넘게 남은 잠금은 죽은 녹화로 보고 지웁니다. 녹화가 끝나면 초당 프레임 수를 알려 주고, 15프레임 아래면 다시 찍으라고 경고합니다.

녹화가 통계에 섞이지 않게

녹화는 실제 플레이어처럼 집계되면 안 됩니다. 녹화 도구는 분석·추적 요청, 사이트 자체의 이벤트 기록 주소, 그리고 판이 끝날 때 게임이 보내는 도전장·방문 기록을 막습니다. 스크립트도 순위에 아무것도 남지 않는 모드를 고릅니다. 4구 당구는 기기 안에서 도는 AI 대전, NEON ORBIT은 게스트 연습판이고, 투비트는 기록·도전장 요청까지 따로 막습니다.

몇몇 게임은 서버나 로그인이 있어야 녹화할 수 있습니다. 이런 게임은 별도 스크립트로 운영 계정·저장·점수가 전혀 없는 로컬 게임 서버를 띄워 찍습니다. 이 서버는 마지막 git 커밋을 묶은 사본으로 돌릴 수 있어서, 제 작업 폴더에서 고치던 중인 코드가 공개 영상에 나오는 일이 없습니다.

16:9와 9:16에 맞추기

사이트 미리보기는 가로 영상입니다. preview.mjs가 데스크톱 녹화를 1280 × 720으로 줄이고 960 × 540 포스터 이미지를 만듭니다. 필요하면 페이지 머리말을 잘라 내고, 아무 일도 없는 구간을 빼고 남은 구간을 이어 붙이고, 필름 잡음이 많은 게임은 잡음을 살짝 걷어 냅니다. UFO는 그 잡음 때문에 파일이 열 배 넘게 커졌습니다.

쇼츠는 1080 × 1920입니다. 1080 × 1680 휴대폰 녹화가 화면을 채우고, 그 위에 게임 이름을 적은 240픽셀 띠가 붙습니다. 끝에는 마지막 플레이 화면을 멈춰 둔 채 2.5초짜리 끝 장면이 서서히 나타나므로, 플레이 장면 자체가 잘리지는 않습니다. 가로 녹화밖에 없는 게임은 화면 가운데에 두고 뒤를 같은 영상을 흐리게 깔아 채웁니다. 투비트의 세 모드처럼 모드가 여럿인 게임은 따로 찍은 영상을 이어 붙이고 구간마다 이름표를 붙입니다.

영상에 담기지 않는 것

방법을 솔직하게 밝히려면, 영상이 보여 주지 않는 것도 적어야 합니다.

  • 플레이하는 것은 스크립트입니다. 사람처럼 보이도록 박자를 다듬었지만, 사람처럼 망설이지는 않습니다.
  • 장면은 고른 것입니다. NEON ORBIT은 처음 30초가 바쁜 시드를 골랐고, 투비트 쇼츠의 첫 구간은 네온 줄넘기 어려움에서 노트가 가장 촘촘한 대목입니다. 앞부분은 녹화 없이 쳐서 콤보를 쌓아 둔 뒤 찍었습니다.
  • 게임 둘레의 화면 요소는 숨겼습니다. 실제 게임에서는 포털 메뉴·공유·설정 단추와 4구 당구의 코칭 안내도 보입니다.
  • DOGFIGHT와 MECH MARS는 게임에 들어 있는 자동 조종·데모 조종사가 조종하고, 녹화 동안에는 내 기체가 격추되지 않습니다. 영상이 중간에 끝나지 않게 하려는 것입니다.
  • 이런 훅은 녹화용 브라우저 안에만 있습니다. 핀볼 훅이 고치는 운영 코드가 바뀌면, 훅은 엉뚱한 화면을 조용히 찍는 대신 오류를 내고 멈춥니다.

플레이를 누르는 사람에게 이것이 중요한 이유

미리보기는 게임이 어떻게 보이고, 어떤 소리가 나고, 어떻게 움직이는지에 대한 약속입니다. 영상이 여러분이 여는 것과 같은 판에서 나오므로, 영상 속 테이블·물리·그래픽·소리가 곧 여러분이 받는 것입니다. 2026-10-08에 NEON ORBIT 테이블 디자인을 바꿨을 때도, 예전 테이블이 나오는 영상을 그대로 두지 않고 그날 바로 미리보기를 다시 찍었습니다.

Four-ball Billiards → NEON ORBIT →