본문으로 건너뛰기

블로그에 미니게임 만들어 넣기

· 약 10분
Wonkook Lee
Product Engineer · Former Industrial Designer

오늘은 레블 1100 MT를 출고한 지 꼭 1년이 되는 날이다. 거창한 기념 계획은 없었지만, 주말 아침 블로그를 열어 이 바이크로 할 수 있는 작은 것을 하나 남겨보고 싶었다.

그렇게 시작한 작은 장난이 블로그 안에서 달리는 오토바이 게임이 됐다. 산과 가드레일을 배경으로 내가 타는 바이크가 달리고, 차선을 바꾸며 콘과 장애물을 피한다.

산길을 배경으로 오토바이가 차선을 달리며 콘을 피하는 완성된 미니게임 화면

블로그에 만든 오토바이 미니게임.


레블 1100 MT와 보낸 첫해

작년 오늘 레블 1100 MT를 데려왔다. 1년 동안 함께 달리다 보니 이제는 검은 차체의 비율과 짧은 앞 휀더, 머플러와 주황색 반사판 같은 작은 부분까지 눈에 익었다. 뒤에서 이미지 시안을 만들며 그 몇 픽셀짜리 디테일에 자꾸 집착하게 된 것도 아마 그 때문일 것이다.

레블 1100 MT에 앉아 있는 라이더의 사진

지난 1년 동안 레블 1100 MT와 함께 달리며 남긴 사진 중 하나.

출고 1년을 기념하는 방법이 꼭 멀리 달리거나 거창한 기록을 남기는 것일 필요는 없을 것 같았다. 오늘은 이 바이크를 블로그 안의 또 다른 도로에 올려보기로 했다.


시작은 인스타그램에서

별생각 없이 인스타그램을 넘기다 오토바이 한 대가 픽셀로 된 산 앞을 달리는 게시물에서 손가락이 멈췄다. 인터넷 연결이 끊긴 화면처럼 No Signal이라고 적혀 있었고, 아래에는 연료를 확인하고 달리러 나가라는 문장이 붙어 있었다. 오류 화면과 라이딩을 섞은 작은 농담이 꽤 마음에 들었다.

미니게임의 출발점이 된 doctor_throttle 인스타그램 게시물

시작은 인스타그램에서 본 doctor_throttle의 게시물이었다.

보는 데서 끝내도 됐지만, 곧 이런 생각이 들었다. 저 화면 속 오토바이가 내가 타는 바이크라면 어떨까. 그리고 바라보기만 하는 이미지가 아니라 실제로 조작할 수 있는 게임이라면.


먼저, 내 바이크로 바꿔보기

처음부터 게임을 만들겠다고 달려든 것은 아니었다. 인스타그램 화면을 캡처하고 내 사진과 오토바이 측면도를 GPT에 건넸다. 이 분위기는 유지하되 바이크와 라이더를 내 모습으로 바꾸어 달라고 했다.

산을 배경으로 라이더와 혼다 레블 1100이 함께 있는 원본 사진

라이더의 복장과 실제 바이크를 보여준 사진.

혼다 레블 1100의 왼쪽 측면 레퍼런스 이미지

차체 비율과 부품 형태를 잡기 위한 측면도.

내 사진은 검은 헬멧과 재킷, 사이드백을 포함한 전체 분위기를 알려주는 자료였다. 하지만 바이크가 비스듬히 놓여 있어 부품의 비율을 정확히 읽기는 어려웠다. 그래서 정측면에서 찍힌 레블 1100 이미지도 함께 넣었다. 사람의 모습은 첫 번째 사진에서, 휠과 머플러, 휀더 같은 구조는 두 번째 이미지에서 가져오게 한 셈이다.

잠시 뒤 익숙한 검은색 바이크가 산을 배경으로 서 있는 이미지가 나왔다. 픽셀풍의 하늘과 산, No Signal이라는 문구는 원래 게시물의 인상을 닮았지만, 화면 안에는 내가 타는 바이크와 자유로 1100 표지석이 들어가 있었다. 남의 게시물에서 시작한 장면이 처음으로 내 이야기에 가까워진 순간이었다.

GPT로 만든 내 바이크와 라이더의 픽셀풍 No Signal 이미지

GPT에 내 바이크와 라이더 사진을 보여주고 만든 첫 이미지.


정지 화면을 움직여보기

이미지를 얻고 나니 이번에는 바퀴가 돌아가는 모습이 보고 싶어졌다. GPT로 만든 이미지를 나노바나나에 넣고, 오토바이가 실제 게임 화면처럼 앞으로 달리는 영상을 만들어 달라고 했다.

나노바나나로 움직임을 더한 영상.

영상 속에서는 바퀴가 돌고 차체가 노면을 따라 조금씩 움직였다. 짧은 장면이었지만 여기서 게임의 움직임을 구체적으로 상상할 수 있었다. 라이더는 몇 컷으로 나누어야 할지, 바퀴는 어떻게 회전해야 할지, 쇼바가 눌리는 느낌은 어느 정도여야 할지 같은 질문이 뒤따랐다.

이제 영상처럼 보이는 것을 넘어, 키를 누르면 반응하는 것을 만들어보고 싶어졌다. 그때부터 클로드와 GPT를 번갈아 열어놓고 실제 게임을 만들기 시작했다.


영상에서 게임용 스프라이트로

영상은 움직임을 상상하기에는 좋았지만 그대로 게임에 넣을 수는 없었다. 같은 바이크가 매 프레임 일관된 형태를 유지해야 했고, 라이더와 차체의 흔들림, 바퀴의 회전, 쇼바의 움직임을 게임이 제어할 수 있는 단위로 다시 나누어야 했다.

그래서 라이더는 열두 컷으로 만들고, 산과 도로, 구름, 가드레일과 장애물도 각각 별도의 이미지로 정리했다. 게임에서는 이 조각들을 한 장에 모은 스프라이트 시트에서 필요한 위치만 잘라 그린다.

열두 컷의 오토바이 라이더와 산, 도로, 구름, 장애물을 모은 게임용 스프라이트 시트

실제 미니게임이 사용하는 스프라이트 시트. 라이더 12컷과 배경, 도로, 장애물이 한 장에 담겨 있다.


점프하지 않는 오토바이 게임

처음 떠올린 건 크롬 브라우저의 공룡 게임 같은 모습이었다. 달리고, 장애물이 나타나고, 피하지 못하면 끝나는 게임.

다만 커다란 크루저 바이크가 바위 앞에서 폴짝 뛰어오르는 장면은 아무리 생각해도 어색했다. 그래서 점프 대신 차선을 바꾸기로 했다. 앞쪽 차선과 뒤쪽 차선을 오가며 콘과 드럼통, 바위를 피한다. 규칙은 그걸로 충분했다.

클로드와 GPT에는 같은 일을 정확히 반으로 나눠 맡기지 않았다. 한쪽에는 기존 블로그의 구조를 읽히고, 다른 쪽에는 이미지와 수정 요청을 던졌다. 그러다 막히면 서로의 결과를 다시 보여줬다. 코드는 클로드, 그림은 GPT처럼 깔끔하게 역할이 나뉜 것도 아니었다. 둘 다 코드를 만졌고 둘 다 시각적인 문제에 의견을 냈다. 나는 그 사이에서 결과를 보고 계속 “이건 아닌데”라고 말하는 사람이 됐다.

게임 자체는 생각보다 빨리 움직였다. 화면이 열리고, 도로가 흐르고, 키를 누르면 오토바이가 차선을 바꿨다. 여기까지만 보면 정말로 AI와 함께 한나절 만에 게임을 만든 셈이었다.

하지만 움직이는 것과 그럴듯하게 움직이는 것 사이에는 제법 긴 거리가 있었다.


게임을 넣되, 블로그는 무겁게 만들지 않기

게임을 만드는 것만큼 중요하게 생각한 조건이 하나 있었다. 게임을 하지 않는 방문자에게 그 비용을 떠넘기지 않는 것이었다. 이곳의 본래 용도는 글을 읽는 블로그다. 몇 번 플레이할 작은 장난 때문에 모든 페이지의 첫 로딩이 느려지거나 기존 키보드 동작이 달라져서는 안 됐다.

그래서 평소 페이지에는 단축키를 감지하는 아주 작은 트리거만 올렸다. 이 트리거가 ↑↑↓↓←→←→ 순서를 기억하고 있다가 마지막 키까지 정확히 들어왔을 때 비로소 게임을 연다. 검색창이나 입력란에 포커스가 있을 때는 방향키를 세지 않고, 키를 길게 눌러 생기는 반복 입력도 무시한다. 모바일에서는 로고를 빠르게 다섯 번 누르는 동작을 같은 입구로 썼다. 첫 번째 탭은 원래처럼 홈으로 이동하게 두어 기존 네비게이션도 건드리지 않았다.

게임 본체는 동적 import()로 분리했다.

import("@site/src/components/RideGame").then((module) => {
setGame(() => module.default);
});

이렇게 하면 빌드할 때 게임 엔진과 캔버스 UI가 블로그의 기본 자바스크립트에서 별도 청크로 갈라진다. 평소 글만 읽는 방문자는 그 청크를 내려받지 않는다. 단축키나 다섯 번 탭으로 게임을 연 순간에만 별도 파일을 요청한다.

용량이 큰 스프라이트 시트도 같은 순서로 미뤘다. 게임 청크가 도착하고 화면에 캔버스가 만들어진 뒤에야 /img/game/ride-sprites.png를 불러온다. 게임을 닫으면 애니메이션 루프와 게임용 이벤트 리스너를 정리하고, 한 번 받은 청크와 이미지는 브라우저 캐시에 남겨 다음 실행을 빠르게 했다.

결국 평소 블로그에 추가되는 것은 짧은 키 입력 감지 코드뿐이다. 게임은 블로그 안에 있지만, 플레이하기 전까지는 로딩과 렌더링의 경로에서 빠져 있다. 눈에 띄는 기능을 하나 더 넣으면서도 원래 블로그의 동작과 성능을 거의 그대로 유지하기 위해 택한 구조였다.


바퀴는 돌지만 캘리퍼는 돌면 안 된다

처음 만든 라이더 이미지는 얼룩덜룩했다. 투명 배경이라고 생각한 곳에는 흰색 픽셀이 남아 있었고, 오토바이 프레임 사이사이에도 배경색이 끼어 있었다. 산등성이는 위쪽이 잘렸고, 산과 도로를 이어 붙인 자리에는 가느다란 틈이 보였다.

라이더를 열두 컷으로 나누어 움직여 보니 문제는 더 잘 보였다. 사람과 차체가 프레임마다 조금씩 달라져 오토바이가 달리는 게 아니라 떨고 있는 것처럼 보였다. 위아래 움직임을 줄이고, 각 프레임의 기준선을 다시 맞췄다.

바퀴도 그냥 회전시키면 끝일 줄 알았다. 타이어와 디스크가 도는 것까지는 좋았는데, 브레이크 캘리퍼도 함께 돌았다. 바퀴 표면의 하이라이트와 그림자까지 빙글빙글 돌아가니 고무 바퀴라기보다 반짝이는 스티커 같았다. 회전해야 하는 부분과 제자리에 있어야 하는 부분을 나누고 나서야 조금 자연스러워졌다.

쇼바가 눌리는 느낌은 살리되 차체가 출렁이지 않게 하고, 머플러와 주황색 반사판을 되찾고, 헬멧은 내가 쓰는 검은색 쇼에이 Z-8에 맞췄다. 멀리서 보면 몇 픽셀에 불과한 것들인데, 하나가 빠질 때마다 이상하리만큼 눈에 잘 들어왔다.


앞 휀더 하나로 아침이 길어졌다

가장 오래 붙잡은 건 앞 휀더였다.

처음에는 너무 길었고, 다음에는 반쪽이 사라졌다. 바퀴의 곡률과 맞지 않거나 타이어에서 붕 떠 보이기도 했다. 그럴듯한 검은색 곡선을 얹는 것과, 실제 바이크에 달린 짧은 휀더처럼 보이게 만드는 것은 전혀 다른 일이었다.

“조금 더 짧게.”

“바퀴에서 너무 떠 있다.”

“같은 간격으로 원을 따라가야 한다.”

수정 요청은 점점 정비소에서 할 법한 말이 되어갔다. 게임 화면에서는 손톱보다도 작게 보일 텐데 나는 그 부분을 계속 확대해 보고 있었다. AI가 이미지를 한 번 더 만드는 데 드는 시간은 짧아졌지만, 무엇이 어색한지 알아보는 일까지 사라진 것은 아니었다.

오히려 시안을 만드는 비용이 싸지니 취향이 더 바빠졌다. 예전 같으면 적당히 넘어갔을 디테일도 “한 번만 더” 고치게 된다. 도로의 차선 길이를 맞추고, 반복되는 아스팔트가 타일처럼 보이지 않도록 질감을 손봤다. 산 뒤로 별과 구름이 어색하게 비치지 않는지도 확인했다. 길가의 비석은 결국 조그만 다람쥐로 바뀌었다. 게임의 완성도와 큰 관계는 없지만, 지금은 그 다람쥐가 꽤 마음에 든다.


AI와 함께 만든다는 것

이번에는 코드를 얼마나 직접 썼는지 세어보지 않았다. 어느 부분이 클로드의 것이고 어느 픽셀이 GPT의 것인지 가르는 일도 별 의미가 없었다. 중요한 건 내가 아주 짧은 시간 안에 생각을 화면 위에 올리고, 실제로 움직여 보며 판단할 수 있었다는 점이다.

동시에 AI가 결과를 빨리 내놓을수록 내 역할은 더 선명해졌다. 오토바이가 달리는지 확인하는 데서 끝나는 게 아니라, 이 움직임이 내 눈에 자연스러운지, 이 바이크가 내가 아는 그 바이크처럼 보이는지 계속 결정해야 했다.

AI는 “다음 시안”을 지치지 않고 내놓았다. 나는 그 시안에서 돌아가면 안 되는 캘리퍼와 떠 있는 휀더, 프레임 사이의 흰 픽셀을 찾아냈다. 꽤 좋은 주말 아침의 분업이었다.


이제 직접 달려보기

게임을 블로그의 정식 메뉴로 만들지는 않았다. 대신 예전 게임의 치트키처럼 간단한 입력으로 어디서든 열 수 있게 했다. 만드는 과정을 여기까지 읽은 사람이라면 바로 실행해볼 수 있다.

블로그에서 실제로 실행한 미니게임 플레이 화면.

키보드에서 아래 순서를 입력하면 게임이 열린다.

↑ ↑ ↓ ↓ ← → ← →

모바일에서는 상단의 블로그 로고를 빠르게 다섯 번 누르면 된다.

굳이 찾아서 플레이할 만큼 거창한 게임은 아니다. 몇 번 차선을 바꾸고, 장애물에 부딪히고, 다시 시작하는 작은 장난에 가깝다. 그래도 글만 있던 블로그 한구석에 이제 오토바이 한 대가 달리고 있다.

주말 아침에 시작한 일치고는 제법 마음에 든다.


좋은 사람들과 재미있는 일을 하며 열정적이고 즐겁게 살고 싶은 개발자