모형

상업 게임 업계에서는 디렉터나 대표가 되어도 원하는 게임을 만들 수 없습니다. 하지만 기계의 손을 빌리며 작게나마 온전히 작동하는 완결된 제 취향의 게임을 만들 수 있게 되었습니다. 한 주 동안 만든 네 개의 모형에서 무엇을 고치고 무엇을 배웠는지 되돌아봅시다.

모형

브라우저에서 미드웨이 해전이 흘러가는 것을 지켜봅니다. 요크타운이 마지막으로 침몰하는 장면에서 카메라가 낮은 각도로 붙어 함교의 실루엣을 역광으로 받고 급강하 폭격대가 내리꽂히는 몇 초 동안은 재생이 서서히 느려집니다. 그 사이 사건 없이 밤이 지나가는 구간에서는 함대가 항진하는 와이드 샷과 수면 위 실루엣이 번갈아 지나가고 어떤 밤도 실제 시간으로 몇 초 안에 지나갑니다. 이 장면은 누가 시켜서 만든 것이 아니고 어느 회의에서 승인을 받거나 드랍된 적도 없고 마일스톤도 성과 지표도 없습니다. 그런데도 처음부터 끝까지 온전히 돌아가고 전부 제 것입니다.

게임 만드는 일을 오래 직업으로 삼아 왔지만 정작 제가 원하는 게임을 만들 기회는 없었습니다. 이건 직급의 문제가 아닙니다. 오래전 '부분과 전체'에서 우리는 전체를 만들지만 늘 부분만 만들기를 요구받는다고 말한 적 있습니다. 디렉터가 되든 PD가 되든 회사의 대표가 되든 이 사정은 크게 달라지지 않습니다. 상업 게임은 너무 커서 한 사람의 취향이 그 안에 온전히 남을 수 없습니다. 수백 명의 손을 거치는 동안 처음의 그림은 조율되고 타협되고 다른 제약에 맞춰 깎여 나가고 그게 잘못된 일도 아닙니다. 그만한 규모의 물건은 원래 그렇게 만들어집니다. 그런데 기계의 손을 빌리면 한 사람이 작은 전체 하나를 처음부터 끝까지 완성할 수 있습니다. 실물 전함이나 실제 우주를 혼자 만들 수는 없지만 그 모형은 혼자 완성할 수 있습니다. 상업 게임이라는 실물 대신 작지만 온전히 작동하는 모형을 만들 수 있습니다. 모형은 부분이 아니라 전체이고, 진열장에 놓인 정물이 아니라 실제로 돌아가는 물건입니다.

게임만 만든 것은 아닙니다. 오히려 먼저 만든 것은 제 취향에 맞는 필요한 도구들이었고 지금도 계속 만들고 있습니다. 지난 'pytmux'에서 정리한 터미널 멀티플렉서가 그 시작이었고 그 뒤로 퍼포스를 터미널에서 다루는 클라이언트, 후잉 가계부를 다루는 터미널 앱, 윈도우 화면 돋보기, 홈서버를 감시하면서 스스로 복구하는 모니터링 앱, 사내 문서를 한데 모아 검색 가능하게 만드는 파이프라인 같은 것들을 만들어 매일 쓰고 있습니다. 이번 한 주만 해도 pytmux에는 1990년대 도스 시절의 파일 관리자 Mdir를 되살린 플러그인을 붙였습니다. 원본 배포판의 스크린샷 픽셀을 직접 뽑아 색상을 확인하고 나서 지배색이 시안이라는 걸 확정하고 나서야 처음 설정했던 파란색이 근본적으로 틀렸다는 걸 알았습니다. 눈대중으로 맞추던 배색을 측정으로 정했습니다. 홈랩 모니터링 도구에서는 한 달치 경보의 절반이 넘는 양이 정작 장애가 아니라 "운영자가 자리에 없다"는 메타 알림이라는 걸 세어 보고 경보를 즉시 알릴 것과 조용히 자동 복구할 것과 하루 한 번 모아 알릴 것으로 다시 배치했습니다. 여기서는 부끄러운 교훈도 하나 얻었습니다. 테스트가 제 실제 로그 파일을 오염시키지 않게 하라는 규칙을 이미 문서로 적어 두고 그 위험을 정확히 서술해 두고도 격리 코드를 엉뚱한 곳에 넣는 바람에 결국 가짜 커널 패닉 경보를 운영자에게 보내고 말았습니다. 규칙을 알고 적고 인용하는 것과 그것을 실제로 적용하는 것은 다른 일이라는 걸 자동화된 모니터링 도구가 자기 주인을 배신하는 모양으로 배웠습니다. 도구와 게임은 서로를 먹여 살립니다. 게임을 만들다 보면 형상관리나 배포나 모니터링에서 매번 손이 가는 곳이 드러나고 그 부분을 메우려고 도구를 만들면 다음 게임을 조금 더 빨리 만들 수 있습니다. 오늘은 그중 게임 쪽에 집중해 네 개의 모형을 만들면서 각각 무엇을 고치고 무엇을 배웠는지 이야기해 보겠습니다.

첫 번째는 퀘이크[^퀘이크 3 아레나]입니다. 정확히는 퀘이크 3의 유명한 맵 q3dm17을 브라우저에서 되살린 아레나 슈팅 게임입니다. 여기서 배운 것은 대부분 밸런싱에 관한 것입니다. 우주 공간 위에 떠 있는 플랫폼으로 이루어진 맵이라 원작의 넉백 값을 그대로 쓰면 총에 맞아 허공으로 떨어지는 낙사가 지나치게 많았습니다. 눈대중으로 "좀 심한 것 같다"고 판단하는 대신 봇들끼리 붙여 놓고 통계를 뽑는 시뮬레이터를 먼저 만들어 측정해 보니 넉백을 5에서 3.2로 낮추고 중력을 480에서 540으로 올렸을 때 분당 사망이 76에서 70으로, 그중 낙사 비중이 21퍼센트에서 15퍼센트로 줄었습니다. 흥미로운 건 같은 값을 만질 때 한쪽을 고치면 다른 쪽이 의도하지 않게 변할 수 있다는 점입니다. 넉백을 줄이면 낙사는 줄지만 로켓을 자기 발밑에 쏴 그 폭발로 날아오르는 로켓 점프도 같이 약해집니다. 그래서 자기 스플래시는 넉백을 먼저 온전히 적용한 뒤 자기 피해만 절반으로 깎는 식으로 순서를 나눠 낙사는 줄이되 로켓 점프는 살렸습니다. 밸런싱이라는 게 하나의 레버를 조작하는 일이 아니라 서로 얽힌 레버들을 어떤 순서로 계산하느냐의 문제라는 걸 이 한 줄을 고치면서 다시 확인했습니다.

우주 공간 위에 떠 있는 데크들. 점프 패드와 텔레포터가 놓인 이 맵에서는 넉백 강도 값 하나가 낙사를 좌우합니다. 오른쪽 위 킬 로그의 'Nyx shoved Torque into the void'가 바로 그 낙사입니다

퀘이크에서 씨름한 다른 부분은 조준이었습니다. 위에서 비스듬히 내려다보는 시점이라 2차원 커서 하나로 3차원 공간을 조준해야 하는 근본적인 어려움이 있습니다. 커서가 적을 살짝만 벗어나도 수직 조준이 엉뚱한 지형을 기준으로 튀고 거리가 멀수록 그 오차가 커집니다. 이걸 커서 자석과 자동 수직 조준으로 풀되 중요한 원칙을 하나 세웠습니다. 조준 보조는 전부 클라이언트에만 넣고 실제로 맞았는지 아닌지를 정하는 서버의 판정은 절대 건드리지 않는다는 것입니다. 수평 조준은 순수하게 커서 입력으로만 남겨 실력의 여지를 지키고 대신 레일건만은 자동 조준의 원뿔을 아주 좁게 잡아 저격의 어려움을 일부러 유지했습니다. 봇도 처음에는 조준이 비현실적으로 정확해서 마치 에임핵처럼 보였지만 봇마다 조준 실력을 따로 주고 이동 중에는 명중이 급격히 떨어지도록 만들자 비로소 덜 봇처럼 보였습니다. 질주하면서 쏘는 저격수형 봇은 조준 산포가 좌우로 44도까지 벌어져 사실상 못 맞히고 기계처럼 안정적인 봇은 18도에 그쳐 어느 정도 유지합니다. 발을 멈춘 저격수만 치명적이고 봇마다 생김새와 행동을 같은 이름 해시에 묶어 두어 외형으로 다음 행동을 어느 정도 예측할 수 있게 했습니다.

빨간 조준 마커가 봇을 물고 있는 순간. 커서 자석과 착탄 표시는 전부 클라이언트에만 있고 실제로 맞았는지는 서버가 따로 정합니다

정작 크게 배운 것은 두 개의 버그를 잡으면서였습니다. 하나는 벽 너머로 쏘는 핵이었습니다. 조준 값 하나에 무한대를 넣으면 히트스캔의 모든 거부 조건이 무한대와의 비교에서 참도 거짓도 아닌 값으로 모호해져 위치와 시야에 상관없이 맵 전체를 관통해 맞히고 게다가 그 값이 상태를 망가뜨려 모든 접속자의 화면을 오염시켰습니다. 검증되지 않은 숫자 하나가 권위 있는 서버 상태 전체를 망가뜨릴 수 있다는 걸 그 숫자가 유한한지부터 확인하는 코드 한 줄을 넣으면서 배웠습니다. 다른 하나는 "사라지는 저격수"라고 이름 붙인 버그였습니다. 120헤르츠가 넘는 고주사율 화면에서 몇 분 가만히 서 있으면 매 프레임 보내는 입력이 서버의 초당 처리 예산을 넘겨 조용히 연결이 끊기고 다시 접속하면 관전자로 변해 카메라가 봇을 따라 배회했습니다. 재현 기기에서 실제로 겪고 나서야 원인을 찾았고 입력을 화면 주사율과 무관하게 일정 속도로 페이싱하고 서버 예산에 여유를 두고 연결이 끊기면 유령처럼 배회하는 대신 "다시 참가하라"고 명시적으로 알리도록 고쳤습니다. 밸런싱이든 넷코드든 추측한 것과 실제 기기에서 벌어지는 일은 자주 어긋나고 결국 측정해 보고 겪어 보는 수밖에 없다는 게 퀘이크에서 얻은 결론입니다.

두 번째는 개미집입니다. 여왕개미 한 마리로 시작해 굴을 파고 애벌레를 기르고 일개미가 역할을 나누고 자라나는 시뮬레이션입니다. 여기서 배운 것은 시뮬레이션 모델을 유지한 채로 개미들의 행동을 조율하는 일이 얼마나 까다로운가 하는 것입니다. 이 게임은 개미를 직접 명령하는 게임이 아닙니다. 먹이를 놓고 페로몬을 뿌리고 농장을 지정하는 식으로 환경만 바꾸면 자율적으로 움직이는 개체들이 알아서 일을 나눕니다. 이번 주에 가장 크게 손댄 것도 바로 이 개입의 감각이었습니다. 한 분이 "산호미포함 정도로 간섭하면 어떨까요, 집이 계속 그대로에요"라는 피드백을 주셨습니다. 정확한 지적이었습니다. 개입 수단은 여럿 있지만 그 결과가 개미집을 눈에 띄게 바꾸지 못하고 있었습니다. 원인을 코드까지 따라가 봤습니다. 결정론과 밸런스를 지키려고 농장 산출을 전부 정규 수입에 흡수시켜 두는 바람에 애써 농장을 돌봐도 콜로니의 반송 능력이 달라지지 않게 만들어 놓은 것이었습니다. 무해하게 만들려던 설계가 무감각을 만들었습니다.

여기서 중요한 판단을 하나 했습니다. 개입에 결과를 붙이자고 해서 새 조작이나 새 메뉴나 새 수치 대시보드를 늘리면 그건 이 게임이 관찰하는 게임이라는 정체성을 깨는 일입니다. 그래서 "새 동사를 늘리는가"를 판단 기준으로 삼아 늘리는 방향은 전부 거부하고 기존 개입에 결과만 붙이기로 했습니다. 먹이와 농장을 꾸준히 돌보면 부양력이라는 값이 올라 콜로니가 그 천장까지 자라도록 하되 예전에 이미 검증해 둔 급식 보정 방식을 농장에도 그대로 일반화해 실측으로 1.77배까지 번성하게 했습니다. 깊이 파면 광맥을 만나 미네랄이 쏟아지거나 대수층을 건드려 국소적으로 침수되거나 용암벽에 막혀 다른 길로 돌아가게 했습니다. 계절이 바뀌면 폭염과 가뭄과 한파가 스쳐 가되 절대 콜로니를 죽이지는 않게 했고 침수된 물이 근처 농장을 적시면 오히려 성장이 빨라지는 작은 순환을 추가했습니다. 미네랄을 소모하는 상점 같은 것도 검토했지만 관찰형 정체성에 안 맞아 보류했습니다. 새 동사 없이 기존 개입에 인과만 붙이는 이 방식이 관찰하는 게임에 손맛을 주는 거의 유일한 길이라는 걸 이 작업에서 배웠습니다.

부양력이 146까지 올라 '자라는 중'이 뜬 콜로니가 깊이 파 내려가다 광맥을 뚫은 장면. 새 조작을 늘리지 않고 기존 개입에 결과만 추가했습니다.

개미집에서 가장 인상적이었던 건 창발 버그를 잡는 과정이었습니다. 비가 오면 개미들이 입구에 공처럼 뭉쳐 못 들어가는 문제가 있었습니다. 입구를 넓히거나 대피 반발을 키워 흩어 놓는 것처럼 직관적으로 "퍼뜨리는" 개입은 하나같이 상황을 악화시켰습니다. 한참을 헤매다 알아낸 진짜 원인은 봉쇄의 타이밍이었습니다. 날이 흐려지면 입구 봉쇄를 시작하도록 되어 있어서 멀리 채집 나갔던 개미들이 뒤늦게 봉쇄된 문 앞에 도착해 갇혔던 것입니다. 다들 돌아온 뒤에야 문을 닫고 반발도 완전히 면제하는 대신 0.3배로만 약화하도록 바꾸자 입구에 뭉친 개미 수가 평균 14.8마리에서 9.7마리로 줄었습니다. 이 버그는 규모가 커야만 드러나서 개미가 128마리쯤 되는 큰 콜로니를 자연스러운 날씨로 돌려야 재현됐습니다. 자율적으로 움직이는 개체들의 행동을 조율할 때는 눈에 보이는 증상에 직접 손대는 쪽이 대개 틀리고 그 증상을 낳는 규칙의 타이밍을 찾아야 한다는 것을 여기서 확실히 배웠습니다. 비슷한 교훈을 사진 도감에서도 얻었습니다. 화면을 가득 채운 무지개를 찍었지만 도감에 추가되지 않는 버그가 있었습니다. 서버는 "찍은 프레임의 위쪽이 지표 위인가"로 판정하지만 클라이언트는 무지개를 깊이 검사 없이 그려 개미집 속을 들여다보는 이 게임의 주 시점에서는 무지개가 지표 아래 흙 위까지 겹쳐 그려졌습니다. 판정과 렌더가 서로 다른 기준을 쓰면 "보여도 기록되지 않는다"가 된다는 것과 판정은 반드시 피사체의 실제 위치를 봐야 한다는 것을 이 일로 배웠습니다.

세 번째는 우주입니다. 지난 '관측 가능한 우주'에서 오래전부터 만들고 싶었지만 엄두가 안 나던 우주 게임을 택시 안에서 프롬프트를 넣기 시작해 만들었다고 말했었습니다. 그 게임을 계속해서 개선해 나가고 있습니다. 여기서 가장 마음에 드는 점은 이 우주가 그림 에셋 하나 없이 오로지 관측된 데이터만으로 그려진다는 것입니다. 밤하늘의 별은 가이아가 관측한 백만 개가 넘는 실측 카탈로그를 실제 좌표와 밝기와 색으로 설정했고 화성과 달의 지형은 레이저 고도계가 실측한 표고 격자를 그대로 사용합니다. 화면에 보이는 산이 곧 착륙하는 산입니다. 다른 항성계의 행성들도 지어낸 것이 아니라 실제로 관측된 외계행성만 실측 궤도로 배치했고 태양계를 실제로 지나간 성간 방문자나 최근 처음 검출된 외계 위성 같은 것들까지 발견되는 대로 반영했습니다. 아직 형체가 밝혀지지 않은 천체는 지어내지 않고 매끈한 채로 둡니다. 발견된 것만 그리고 모르는 것은 모르는 채로 두는 것이 관측된 정보만으로 그려낸 우주를 오히려 그럴듯하게 만든다는 걸 만들면서 알았습니다. 광속의 한계나 상대론적 시간 지연이나 궤도역학처럼 게임을 불편하게 할 법한 물리도 없애는 대신 규칙으로 삼았습니다. 목적지를 예약해 두고 실제로 걸리는 시간이 흐르기를 기다리는 방치형 항해의 리듬이 오히려 그 물리 덕분에 성립합니다.

에셋 없이 관측된 별의 좌표와 밝기와 색만으로 그린 성운

이번 주에 우주에서 다룬 일들은 대개 그 데이터 세계를 더 가볍고 더 살아 있게 만드는 것이었습니다. 게임을 로드할 때 6.8메가짜리 별 카탈로그를 통째로 받고 나서야 하늘이 완성되던 것을 밝은 별을 먼저 띄우고 나머지는 바라보는 방향의 하늘 조각부터 흘려받도록 나눴습니다. 이때 지켜야 할 제약은 "화면에 보이는 결과는 이전 상태와 비교해 한 픽셀도 달라지지 않아야 한다"는 것이어서 데이터가 언제 어디에 상주하느냐만 바꾸고 별을 솎아 내는 계산은 그대로 두었더니 밝은 별들의 정점 수가 예전과 정확히 일치했습니다. 서버가 직접 조종하는 데모용 우주선도 하나 만들었습니다. 별도의 렌더링 트릭이 아니라 진짜 헤드리스 플레이어로 만들어서 시뮬레이션 코어를 건드리지 않고도 다른 접속자의 레이더에 자연스럽게 포착되도록 했습니다. 목적지도 게임감 있게 가까운 천체를 자주 고르되 풀 전체를 골고루 도는 편향 랜덤으로 골라 "가까운 데를 자주, 그래도 다양하게" 다니게 했습니다. 조명에서는 한 가지를 확실히 배웠습니다. 우주선이 태양을 등지면 본체가 새까맣게 보이지 않게 되는 문제를 오래 씨름했습니다. 순수한 측면 시점에서는 어떤 방향광으로도 물체를 밝힐 수 없고 스스로 은은하게 빛나는 바닥값만이 각도와 하드웨어에 상관없이 가시성을 보장한다는 결론에 이르렀습니다. 위치를 가진 광원은 모델링을 위한 것이고 최후의 가시성은 자기발광이라는 이 구분은 앞으로도 오래 쓸 교훈입니다.

네 번째가 처음에 이야기한 미드웨이입니다. 이 게임은 이번 한 주 안에 만들어졌습니다. 1942년 미드웨이 해전을 세 차례에 걸친 사료 조사로 재구성해 서버가 결정론 엔진으로 만든 사건의 저널을 클라이언트가 재생하고 다섯 개의 결정적 순간에서만 역사대로 갈지 다른 선택을 할지 개입할 수 있습니다. 나흘 만에 데이터셋부터 결정론 엔진, 2차원 재생기, 개입 UI, 3차원 플레이어, 공개 사이트, 데이터 암호화까지 한 파이프라인을 완주했습니다. 여기서 배우고 있는 것은 영화적 연출을 게임에 적용하는 요령입니다. 조작이 없는 관전에는 고유한 페이싱 문제가 있습니다. 고정 배속으로는 야간 초계 같은 공백은 지루하고 급강하 폭격의 몇 초는 순식간에 지나칩니다. 그래서 마흔여덟 개의 사건마다 창을 부여해 겹치는 구간에서 배속을 연속으로 조절하되 공백은 최대 120배로 건너뛰고 사건은 1배로, 급강하 순간은 0.4배로 늦췄습니다. 처음에 감속과 가속을 같은 곡선으로 부드럽게 처리했더니 고배속에서 감속이 사건을 따라가지 못해 놓치는 문제가 있어 감속은 빠르게 처리하고 가속만 부드럽게 하는 비대칭 방식으로 바꿨습니다. 그러자 마흔여덟 개 사건이 전부 2배 이하로 감속돼 재생되고 액션이 차지하는 실제 시간 비중이 20퍼센트에서 42퍼센트로 올랐습니다.

급강하 폭격을 맞은 아카기가 갑판 한복판에서 불타오르고 검은 연기가 솟고 있습니다. 아래 'AUTO'와 '극적' 버튼이 배속과 연출을 쥐고 적응형 감속이 이 급강하의 몇 초를 0.4배까지 늦춰 지나가지 않게 만듭니다

카메라는 처음에 그저 대상과 거리만 부드럽게 따라갔습니다. 그러면 앵글이 임의적이어서 밋밋했습니다. 그래서 샷의 크기와 앵글, 골든아워의 역광, 다음 동작을 미리 준비하는 앤티시페이션 같은 영화 문법을 조사해 사건 각각에 매핑했습니다. 이함하는 함재기는 9도의 낮은 각도로, 명중은 24도로, 침몰은 골든아워의 역광 실루엣으로 잡고 항모 네 척이 사망타를 맞는 순간에는 화각을 46도에서 32도로 좁히는 돌리 줌을 사용했습니다. 컷이 바뀔 때는 공간을 넓게 여는 확립샷으로 시작해 3분 남짓한 비트 동안 서서히 줄여 갑니다. 이 "확립하고 나서 붙는" 리듬이 영화 편집의 기본 문법이라는 걸 만들면서 알았습니다. 사건이 없는 긴 공백은 빠르게 재생해 날려 버리는 대신 달이나 일출이나 수면 실루엣 같은 연출로 채웠습니다. 여기서도 페이싱의 원칙을 하나 배웠습니다. 감상용 감속은 배속의 절대값이 아니라 공백의 길이에 대한 실시간 상한으로 설계해야 밤이 길든 짧든 일관됩니다. 관전과 개입은 언뜻 양립하기 어렵습니다. 관전이 성립하려면 결과가 정해져 있어야 하고 개입이 성립하려면 결과가 열려 있어야 하기 때문입니다. 이 모순은 두 상태를 시간축에서 나눠 개입 지점 이전은 실제 역사를 고정하고 개입하는 순간 이후의 앵커를 모두 풀어 규칙만으로 진행하는 방식으로 만들었습니다.

미드웨이에서 배운 것 중에는 연출 바깥의 것도 있습니다. 권위 있는 저널을 클라이언트가 평문으로 받아 가면 게임을 통째로 복제하기 쉬워서 요청할 때마다 일회용 키를 교환해 저널을 봉인해 보냈습니다. 그러고 나서 스스로 공격해 봤습니다. 암호 자체는 열 개의 공격을 견뎠지만 예전에 만들어 둔 다른 통로 하나가 같은 저널을 평문으로 그대로 뱉고 있다는 걸 발견해 그 통로를 없앴습니다. 내가 잠근 문이 정말 잠겼는지 확인하는 유일한 방법은 내가 직접 그 문을 따 보는 것이라는 당연하지만 자주 잊는 사실을 여기서 다시 확인했습니다. 성능에서도 한 가지 배웠습니다. 교전 중에 예광탄과 폭발이 매번 새 객체로 생성돼 메모리를 압박하길래 종류별로 풀을 만들어 재사용했더니 교전 한 번에 새로 만드는 객체가 1032개에서 69개로, 프레임당 할당이 7.53개에서 0.51개로 줄었습니다. 화면에 보이는 화려함과 그 아래의 비용은 별개로 관리해야 한다는 오래된 이야기의 재확인이었습니다.

한 주의 마지막 날은 결이 조금 달랐습니다. 이날 하루는 네 모형 전부에서 거의 전부가 보이고 느껴지는 것을 다듬는 데 들어갔습니다. 미드웨이에서는 태양의 반대방향으로 움직이던 달을 실측 궤도로 수정했습니다. 보름달을 1942년 5월 30일로 잡고 삭망월 주기로 계산하니 6월 4일 일몰 직후의 바다는 실제 역사처럼 달 뜨기 전의 캄캄한 밤이 되고 달은 밤 11시 반에야 동남동에서 떠올라 새벽까지 하늘에 걸립니다. 태양과 달이 함선과 함재기에 그림자를 드리우게 했고 밋밋한 평면이던 바다에는 파장이 다른 세 겹의 너울을 깔았고 멀리서 잘 보이게 하기 위해 4배로 키워 두었던 함재기는 카메라가 다가가면 실물 비율로 연속 수렴하게 해서 250미터 항모 옆에 선 12미터 전투기의 크기가 근접 관전에서 비로소 실제 크기가 됩니다. B-17이 급강하 폭격을 하던 역사 오류도 이날 바로잡았습니다. 실제 역사대로 고공에서 수평으로 지나가고 표적함 주위에 물기둥 숲만 세우고 한 발도 맞히지 못합니다. 명중이 없는 쪽이 고증인 폭격은 이 게임이 아니었다면 만들 일이 없었을 겁니다. 우주에서도 같은 결의 일을 했습니다. 444광년을 날아 플레이아데스에 도착해도 성단이 여전히 배경의 그림이던 텅 빈 공간을 일곱 자매의 실측 좌표와 밝기와 분광형으로 채웠습니다. 이때도 사실성의 선을 지켰습니다. 하늘에서 보이는 배치는 측정값 그대로 재현하되 측정 정밀도가 미치지 않는 별들 사이의 앞뒤 거리는 지어내는 대신 절차 생성입니다. 토성에는 고리가 본체에 드리우는 그림자를 넣었습니다. 행성 스케일에서는 섀도우맵이 심하게 깨지기 때문에 표면의 각 점에서 태양으로 광선을 쏘아 고리 평면과 만나는 반경을 계산하는 해석적인 방식으로 접근했고 덕분에 실제 토성 사진에서 보이는 것처럼 카시니 간극이 그림자에도 저절로 틈으로 남습니다.

개미집과 퀘이크에서는 실측보다 체감 쪽이었습니다. 개미집에는 "귀여운 느낌이 안 든다, 그래픽이 어설프다"는 피드백을 받았습니다. 문화권을 가리지 않고 통하는 아기 얼굴의 신호가 있습니다. 큰 눈, 반짝이는 하이라이트, 홍조, 그리고 깜빡임입니다. 이걸 공통 부품으로 만들어 개미부터 달팽이와 지렁이와 생쥐까지 방문객 전 종에 달고 새는 파닥이다 활공하고 나비는 이따금 날개를 모으는 식으로 움직임도 종마다 다르게 만들었습니다. 땅속에는 종을 알아맞힐 수 있는 공룡 화석 골격을 심었습니다. 티라노사우루스는 깊은 턱과 앙증맞은 앞다리로, 트리케라톱스는 목의 프릴로 알아보고 클릭하면 종 이름을 알려 줍니다. 지구 반대편까지 파 내려가면 화면을 뒤집지 않고 그대로 계속 내려가 하늘과 땅이 뒤집힌 세계에 도착하게도 했습니다. 퀘이크에는 타격감을 네 겹으로 다시 입혔습니다. 피격의 흔들림과 공격받은 방향 표시, 무기마다 다른 반동과 발사음의 묵직함, 대미지에 따라 음이 올라가는 타격음과 킬 확정음, 그리고 로켓이 귓가를 스칠 때 소리가 왼쪽 귀에서 오른쪽 귀로 훑고 지나가는 니어미스까지. 이것도 앞서 이야기한 조준 보조와 같은 원칙 위에 있습니다. 전부 클라이언트 연출이고 서버 판정은 한 줄도 건드리지 않았고 멀미가 있는 분을 위해 화면 효과만 끄는 스위치를 함께 넣었습니다. 멀미에 대해서는 나중에 따로 이야기할 기회가 있을 겁니다.

이 하루가 남긴 교훈은 뜻밖에 테스트에 관한 것입니다. 화면을 만지는 작업은 겉이 화려한 만큼 검증이 취약해서 상태 플래그와 수치 검증은 전부 통과하지만 화면에는 아무것도 없는 일이 이날 하루에만 세 번 있었습니다. 성단을 채우고 처음 찍은 캡처는 페이드가 오르기 전에 찍혀 텅 빈 하늘이었고 화성의 먼지폭풍 셸은 실측 지형의 산 아래에 통째로 묻혀 있었고 고리 그림자는 값이 전부 켜졌지만 정작 토성 본체가 프레임 밖이었습니다. 검증이 통과한다는 것과 보인다는 것은 다른 말이라는 걸 세 번 반복해서 배우고 시각적 주장은 반드시 눈으로 확인한 뒤에만 배포한다는 것을 규칙으로 정했습니다. 그 규칙의 대가도 이날 바로 치렀습니다. 화성 먼지폭풍은 셸을 지형 위로 띄우고도 여전히 보이지 않았습니다. 섀이더가 투명도에 태양 입사각을 곱하고 있어 밤 반구에서는 어떤 값을 넣어도 0이 된다는 것까지는 알아냈지만 태양 쪽에서 찍는 세 가지 방법이 전부 실패했습니다. 그래서 눈으로 확인하지 못한 강도 상향은 배포하지 않았고 스크린샷으로 보여 주는 페이지에서 화성 섹션을 내렸고 실패한 세 가지 방법을 다음에 같은 길을 다시 걷지 않도록 그대로 기록해 뒀습니다. 곱해지는 항 하나가 0이면 다른 항을 아무리 키워도 0이라는 적어 놓고 보면 당연한 산수를 화면 앞에서 배우는 데 한나절이 들었습니다.

이렇게 겉모습만 만든 것은 아닙니다. 네 개의 모형을 만들면서 그 아래를 받치는 기술적인 주제들도 함께 배우고 있습니다. 가장 큰 축은 서버 권위 시뮬레이션입니다. 네 게임 모두 서버가 계산하고 클라이언트는 재생만 한다는 원칙을 공유합니다. 화면에 보이는 것과 실제로 일어난 일을 나눠 두면 조준 보조 같은 편의는 클라이언트에 마음껏 넣으면서도 승패를 정하는 판정은 한 자리에 뭉쳐 지킬 수 있습니다. 앞서 퀘이크의 조준 보조나 개미의 사진 판정에서 이 구분이 왜 중요한지가 반복해서 드러났습니다. 두 번째는 골든을 세우고 유지하는 일입니다. 결정론 엔진은 같은 입력에 언제나 같은 결과를 내야 합니다. 이걸 대표 시나리오의 상태를 해시로 기록해 잠급니다. 미드웨이라면 개입 없이 재생한 저널의 해시가 특정 값이어야 하고 커밋 전에 이 해시가 그대로인지 확인합니다. 그래서 연출이든 사실성이든 화면에만 영향을 주는 변경은 전부 해시를 건드리지 않아야 하고 사료가 실제로 바뀔 때만 해시를 새로 정합니다. 개미집에서 새 개입을 넣을 때도 모든 새 경로를 플레이어 전용 조건 뒤에 두어 골든 시나리오에서는 그 경로에 아예 진입하지 않게 만들었습니다. 계절 위기 같은 것도 골든 시나리오가 늘 봄인 점을 이용해 봄에서는 조건이 먼저 걸러지도록 해 난수의 흐름이 비트 단위로 예전과 똑같이 유지되도록 했습니다. 이 골든이라는 안전망이 있어야 겁 없이 코드를 고칠 수 있다는 것이 네 게임에 걸쳐 얻은 가장 실용적인 교훈입니다.

세 번째는 CPU 부하 최적화입니다. 여기서 배운 것은 최적화 기법 자체보다 측정에 관한 것이었습니다. 개미집에서 보는 사람 없는 콜로니를 얼려 두는 조건 하나를 바꾸자 로컬 실험에서는 지속 CPU가 네 배 넘게 좋아지는 것처럼 보였습니다. 그런데 실제 서버에 올려 재 보니 이득이 사실상 0이었습니다. 이미 대부분의 콜로니에 보는 사람이 없어 남은 비용은 관측 중인 소수 콜로니가 만드는 바닥값이었고 보는 사람이 없는 콜로니를 더 최적화해 봐야 줄어들 것이 없었던 겁니다. 두 가지를 잘못했습니다. 하나는 관측용 지표와 개선을 같은 배포에 함께 올려서 전후 비교를 스스로 날려 버린 것이고 다른 하나는 로컬에서 자라나는 콜로니의 상태 분포가 오래 성숙한 실제 서버의 분포를 대표하지 못한다는 걸 놓친 것입니다. 측정이 직관을 이겼고 이런 일이 몇 번 반복되면서 지속 CPU를 더 짜낼 여지는 이제 거의 소진됐다는 결론을 아쉬운 대로 정직하게 기록으로 남겼습니다. 로컬의 그럴듯한 추론을 실측에 따라 바꾸는 것은 지난 'AI 활용 회고 (2025)'에서 문서를 쓰기 전에 스크립트로 먼저 시뮬레이션해 본다고 말했던 그 이야기와 뿌리가 같습니다. 다만 그때는 추측 대신 시뮬레이션을 믿자는 이야기였다면 이번엔 그 시뮬레이션마저도 실제 환경 앞에서는 의심해야 한다는 이야기입니다.

글을 마무리하기 전에 적어 둘 것이 하나 더 있습니다. 그 사이에 모형이 다섯이 됐습니다. 마지막 날 하루 동안 가위바위보 게임을 만들었습니다. 뼈대를 세우고 서버와 클라이언트를 붙이고 공개 주소로 배포하고 보안 감사에 인계 문서까지 미드웨이가 나흘 걸린 완주를 하루에 마쳤습니다. 게임은 단순합니다. 가위바위보 다섯 판을 미리 입력해 두면 상대가 자리에 없어도 그 시퀀스와 실시간으로 가위바위보하고 이기면 윗 계단으로 올라가고 지면 그 칸에 머무릅니다. 그런데 이 단순한 규칙이 성립하려면 뜻밖에 암호학이 필요합니다. 상대의 손이 서버에 미리 저장되어 있다는 말은 서버가 마음만 먹으면 판을 조작할 수 있다는 뜻이기도 해서 시퀀스를 저장하는 순간 그 해시를 먼저 공개하고 매치가 끝난 뒤에 원문을 공개해 서버가 중간에 손을 바꾸지 않았다는 걸 클라이언트가 스스로 검증할 수 있게 했습니다. 다섯 판의 조합은 243가지뿐이지만 무작위 소금을 섞으면 해시에서 원문을 되짚을 수 없습니다. 공개한 당일의 보안 감사에서는 16가지 보안 문제가 나왔고 전부 고쳤습니다. 그중 가장 치명적인 것은 마지막 판을 이기고 나서 결과 연출이 흐르는 0.4초 사이에 접속이 끊기면 이긴 매치가 몰수패로 뒤집히는 버그였습니다. 승부는 판정이 나는 순간 확정되어야 하고 연출은 그 뒤에 오는 장식이어야 한다는 원칙을 가장 작은 모형에서 다시 배웠습니다.

이 다섯 번째 모형은 태어난 다음 날에도 개발을 계속했습니다. 가위바위보는 단순한 만큼 반복하면 금세 지루해집니다. 그 지루함을 눈대중으로 짐작하는 대신 측정해 보았습니다. 아무렇게나 내면 승률이 21퍼센트 남짓이고 한 칸을 오르는 데 다섯 판 가까이, 꼭대기까지는 사백마흔 판, 예닐곱 시간이 걸렸습니다. 스킬을 표현할 여지가 없고 형식이 하나뿐이고 보상이 밋밋한 게 원인이었습니다. 여기서도 개미집에서와 같은 판단을 했습니다. 미니게임을 새로 붙이거나 돈으로 승률을 사게 하는 식으로 조작을 늘리는 대신 이미 있는 반복에 읽을 거리만 더했습니다. 봇마다 손버릇을 열세 가지로 나누고 그 힌트를 공개해 상대를 읽으면 이기는 스킬 게임으로 바꿨습니다. 그러자 무작위로 낼 때 21퍼센트이던 승률이 힌트를 읽으면 57퍼센트로 오르고 꼭대기까지가 사백마흔 판에서 백일흔네 판으로 줄었습니다. 봇의 이름과 얼굴도 만들었습니다. 행성과 동물 이름을 달고 있던 봇들을 여러 게임에서 온 낯익은 캐릭터 백일곱 명으로 교체했습니다. 원작의 그림이나 로고는 한 조각도 쓰지 않고 마인크래프트풍 복셀로 근사한 팬 오마주이고 외형이든 배경이든 그림 에셋 하나 없이 전부 코드로 생성한다는 퀘이크의 원칙을 그대로 이어받았습니다. 재미있는 건 손버릇을 캐릭터의 성격에 맞춰 설정한 것입니다. 힘으로 밀어붙이는 캐릭터는 바위를, 칼을 쓰는 캐릭터는 가위를, 마법으로 감싸는 캐릭터는 보를 즐겨 내게 해서 힌트를 굳이 읽지 않아도 상대가 누구인지만 알면 다음 수를 어느 정도 짐작하게 했습니다. 대신 로스터의 마지막 다섯은 아무 버릇 없는 완전 무작위로 남겨 읽기만으로는 안 통하는 상대도 두었습니다.

1번 칸에서 마리오와 겨루는 장면. 상단의 '마리오: 바위를 좋아한다'가 봇마다 다른 손버릇 힌트입니다. 상대를 읽으면 이기는 스킬의 여지를 이렇게 만들었습니다. 캐릭터도 배경도 그림 에셋 없이 전부 복셀로 생성했습니다.

캐릭터를 꾸미는 재미도 붙였습니다. 모자와 안경과 옷과 신발과 머리 모양을 상점에서 사서 입힙니다. 사기 전에 그 물건을 미리보기에서 천천히 구경할 수 있습니다. 이 미리보기가 자기만의 조립 방식을 따로 가지면 '상점에서 본 모습'과 '계단에 선 모습'이 달라지기 때문에 외형을 합성하는 코드를 한 곳으로만 두어 둘이 어긋나지 않게 했습니다. 물건은 자기가 이긴 층에서만 살 수 있고 아직 못 이긴 위층의 물건을 사려 하면 그 칸의 캐릭터가 화를 냅니다. 그런데 가장 공을 들인 건 실시간이었습니다. 실시간 대결이어도 상대가 손을 낸 순간이 서로에게 보이지 않아 혼자 플레이하는 느낌이었습니다. 낸 순간을 즉시 중계하되 무엇을 냈는지는 절대 싣지 않도록 했습니다. 무엇을 냈는지가 새면 늦게 내는 쪽이 이기는 손을 골라 게임이 통째로 깨지기 때문에 손의 공개는 예나 지금이나 판정이 나는 한 곳뿐입니다. 로비도 마찬가지로 손봤습니다. 예전에는 남의 대결을 보려면 관전 버튼을 눌러 화면을 통째로 넘겨받아야 했습니다. 이제 계단을 구경하는 동안 남들의 대결이 그 칸 위에서 실시간으로 벌어지고 판정이 나야 양쪽 손이 함께 공개됩니다. 이걸 안전하게 만든 방법이 마음에 듭니다. 중계를 매치 판정과 똑같은 자리 한 곳에서만 내보내도록 묶어 '손은 판정 뒤에만'이라는 규칙이 저절로 상속되게 했습니다. 규칙이 두 곳으로 나뉠 수 없으면 한쪽만 깜빡하고 손을 유출시키는 사고도 일어날 수 없습니다.

로비에서 계단을 구경하는 동안 다른 사람들의 대결이 그 칸 위에서 실시간으로 벌어집니다. 러그와 소품으로 꾸민 자리마다 다른 캐릭터가 서 있습니다

우주도 계속해서 손봤습니다. 이번엔 성운이었습니다. 멀리서 보이는 성운의 배경 그림이 실제 천체사진과 크게 달랐습니다. 방사형 얼룩 위에 고립된 구불구불한 선을 덧그리던 방식이라 낙서한 지렁이 같은 자국이 남았습니다. 이걸 프랙탈 잡음으로 만든 연속적인 가스장으로 다시 그리고 이온화된 정도에 따라 안쪽 핵은 청록으로 바깥은 붉게 색을 입혀 실제 발광성운의 필라멘트와 어두운 먼지 레인을 흉내 냈습니다. 그런데 여기서 앞서 이야기한 교훈을 정확히 한 번 더 되풀이했습니다. 성운 안을 비행하는 연출을 새로 붙였습니다. 상태 값과 검증이 전부 맞아도 정작 구름이 화면에 나타나지 않았습니다. 원인은 섀이더도 지형도 아니고 두 파일이 같은 모듈을 서로 다른 버전으로 불러들이는 바람에 하나의 모듈이 인스턴스 둘로 나뉜 것이었습니다. 한 인스턴스는 구름을 만들었지만 화면에 그리는 쪽은 다른 인스턴스의 텅 빈 값에 근거해 페이드를 0으로 설정하고 있었습니다. 검증이 통과한다는 것과 보인다는 것은 다른 말이라는 걸 이번엔 섀이더도 기하도 아닌 임포트 문자열 한 줄에서 다시 배웠습니다.

성운 안을 비행하는 연출. 프랙탈 가스장으로 다시 그렸습니다. 정작 이 구름은 같은 모듈이 인스턴스 둘로 나뉘는 바람에 한동안 화면에 뜨지 않았습니다

게임 쪽에 집중하겠다고 해 놓고도 그사이 도구 역시 천천히 개발하고 있었다는 것만 짧게 적어 둡니다. 이 회고의 근거가 된 형상관리 이력을 브라우저에서 조회하고 고칠 수 있는 웹 클라이언트를 새로 하나 만들었습니다. 한 주 동안 무엇을 고쳤는지 되짚으려고 매번 입력하던 명령어들을 하나의 서버 프로세스가 뒤에서 대신 실행하고 화면으로 돌려주는 앱입니다. 각자 자기 계정으로 로그인하니 볼 권한이 있는 것만 보입니다. 매일 쓰는 터미널 멀티플렉서에서는 정기 보안 점검에서 제법 뼈아픈 버그를 하나 잡았습니다. 이상한 제어 문자를 잔뜩 보내면 화면을 그리는 계산이 입력 길이의 제곱으로 폭발해 128킬로바이트짜리 입력 하나가 서버 전체를 4.76초 동안 멈추게 만들었습니다. 파이프 한 줄로 악의적인 출력을 넣기만 하면 되는 것이라 이걸 0.023초로 줄이고 나아가 '작업이 화면 루프를 막고 있는가'를 사람이 코드를 읽어 찾는 대신 실행 중에 기계가 스스로 측정해서 걸리게 만들었습니다. 같은 종류의 버그가 네 번이나 반복됐고 매번 사람 눈으로 찾아야 했기 때문입니다. 눈이 자꾸 놓치는 것을 기계가 대신 검사하도록 만드는 이 방향은 이 글 내내 반복된 측정의 이야기와 결국 같은 이야기입니다.

p4-web의 'Changes' 화면. 제출된 변경을 사용자·경로·기간으로 걸러 브라우저에서 넘겨 보는 이 회고의 근거가 된 형상관리 이력 도구입니다

이렇게 적어 놓고 보면 여러 가지 일을 한 것 같지만 어디까지나 모형입니다. 실물 상업 게임의 규모나 완성도와는 비교할 수 없고 이걸 만들었다고 해서 회사에서 원하는 게임을 만들 수 있게 되는 것도 아닙니다. 그 벽은 그대로입니다. 그리고 이렇게 하나하나 무엇을 고쳤고 그래서 무엇을 배웠는지 사실대로 되짚어 적는 일 자체가 사실은 읽는 분들을 위한 것이기도 하지만 그만큼 저 자신을 위한 것이기도 합니다. 겪을 때는 그저 버그 하나를 해결했을 뿐인 일도 이렇게 글로 적어보면 그게 밸런싱의 원칙이었는지 자율 개체를 다루는 요령이었는지 판정과 렌더를 구분하는 문제였는지가 비로소 정리되고 다음 모형을 만들 때 시행착오를 줄이게 해줍니다. 배운 것 중 얼마쯤은 언젠가 회사에서 게임을 만들 때 사용될지도 모르고 아니면 그저 이 모형들이 더 늘어나는 것으로 그칠지도 모릅니다. 어느 쪽이든 오래 부분만 만들어 온 사람에게 전체 하나를 완성해 보는 경험은 생각보다 큰 재미를 주고 또 위안이 됩니다. 그럼 다음에도 기계의 손을 빌려 여러 가지 모형들을 만들어 보겠습니다.