울면서개발하기
KARTORY 개발일지

빈 저장소에서 온라인 카트 레이싱까지 하루 만에

· 커밋 64개

KARTORY 를 처음 만든 날이다. 목표는 브라우저에서 바로 하는 카트 레이싱이고, 핵심은 드리프트와 부스터의 손맛이다. 같은 뼈대로 먼저 만든 FPS(VARENA)의 구조를 참고해 저장소를 세우고, 오후에는 혼자 달리기, 저녁에는 온라인 스피드전과 아이템전, 랭킹까지 갔다.

처음 계획은 “혼자 기록 도전 → 온라인 → 아이템전” 이었는데, 혼자 달리기가 일찍 손에 잡혀서 온라인을 앞당겼다. 작업은 역할별 에이전트(물리·클라이언트·서버·테스트·리뷰)에 나눠 맡기고, 단계마다 먼저 공용 타입과 통신 규약 같은 “계약” 을 정한 뒤 구현을 합치는 순서로 했다.

한 일

혼자 달리기와 조작감

  • 카트 물리를 직접 만들었다. 조향·미끄러짐·드리프트·게이지·부스터·드리프트 끝 순간 부스터·벽·바닥까지 들어 있다
  • 조작감 숫자는 한 파일에 모으고, 개발 중에는 튜닝 패널(F1)에서 슬라이더로 맞춘 뒤 “값 복사” 로 코드에 옮긴다. 공개 버전에서는 패널을 숨겼다
  • 첫 트랙 “초원 순환로”(약 884m)에 이어 눈꽃 계곡(약 1087m, 오르내림과 골짜기 헤어핀), 사탕 마을(약 820m, 헤어핀 셋과 시케인)을 더했다. 트랙마다 풍경 테마가 다르다
  • 혼자 달리기에 트랙별 최고 기록, 최고 기록을 반투명 카트로 다시 보여 주는 고스트, 미니맵을 붙였다
  • 혼자 달려도 심심하지 않게 봇 상대(0~5대)를 넣었다

캐릭터와 화면

  • 2등신 꼬마 레이서 캐릭터와 통통한 장난감 카트, 파스텔 톤 트랙으로 분위기를 잡았다. 툰 재질에 외곽선, 드리프트 때 별 반짝이 불꽃, 벽에 부딪히면 머리가 튕기는 연출까지
  • 캐릭터는 둘이다. 강아지 후드를 쓴 “댕댕이” 와 당나귀 후드에 당근 낚싯대를 단 “머겅이”. 내가 건넨 그림을 바탕으로 시안 몇 가지를 나란히 놓고 비교해 골랐다
  • 시작 화면에서는 카메라가 캐릭터 얼굴을 비추며 천천히 오가다가, 첫 키를 누르면 추격 위치로 넘어간다. 이때 ←→ 로 캐릭터를 고른다
  • 소리는 파일 없이 코드로 합성했다. 엔진·타이어·부스터·카운트다운·완주 효과음과 배경음 두 곡

온라인 스피드전

  • 방에 들어가는 방법은 빠른 참가와 초대 코드(친구 방) 두 가지. 대기 → 카운트다운 → 경주 → 결과 순서로 흘러간다
  • 서버가 60Hz 로 같은 물리를 돌리고 체크포인트·랩·순위·완주·역주행을 판정한다
  • 대기실에서 트랙 투표, 과반이 준비하면 자동 출발, 사람이 적으면 빈자리를 봇이 채우고 사람이 오면 비켜 준다
  • 공개 전에 리뷰를 한 번 돌려 접속 한도, 메시지 검증, 잠수 처리, 멈춘 방 정리 같은 서버 쪽 방어를 보강했다

아이템전

  • 아이템 상자와 다섯 가지 아이템(로켓 사탕·푸딩 함정·추적 풍선·비눗방울·먹구름)을 넣었다. 순위가 낮을수록 좋은 아이템이 나올 확률이 높다
  • 맞으면 카트가 빙글 돌며 잠깐 조작을 못 한다. 봇도 상황을 보고 아이템을 쓴다
  • 스피드전 결과가 아이템전 코드 때문에 바뀌지 않았는지는 미리 찍어 둔 기록과 비트 단위로 비교하는 테스트로 확인했다

휴대폰

  • 화면 버튼 조작(조향·드리프트·부스터, 자동 가속 기본 켬), 가로·세로 배치를 만들었다
  • 작은 화면에서는 픽셀 비율·그림자 해상도·소품·파티클을 줄인다
  • 카트 한 대를 이루는 정지 부품을 재질별로 합쳐서 카트 한 대의 그리기 호출이 약 168번에서 47번으로, 장면 전체는 데스크톱 677번에서 224번으로 줄었다. 삼각형 수와 화면은 그대로다

랭킹, 테스트, 배포

  • 트랙×모드별 전체 기록 TOP 10, 최고 랩 TOP 10 을 서버에 저장하고 메뉴에서 볼 수 있게 했다. 저장은 임시 파일에 쓰고 바꿔 끼우는 방식으로 해서 쓰다 꺼져도 파일이 깨지지 않게 했다
  • 단위 테스트는 하루 끝에 317개가 됐다. 별도 도구(안티그래비티)를 독립 테스터로 두고, 내가 만든 코드를 보지 않은 쪽에서 물리·트랙·경주 판정·메시지 검증 테스트를 따로 쓰게 했다
  • 서버 부하·퍼즈 시험(48명 동시 접속, 이상한 메시지 12종)을 만들어 돌렸다
  • 정적 파일과 게임 서버를 의존성 없는 작은 Node 서버 하나로 묶고, 도커 이미지로 만들어 공개했다

기술 메모

같은 입력이면 같은 결과. 물리는 60Hz 고정 간격이고, 같은 상태·입력이면 언제나 같은 결과가 나오게 만들었다. 틱 안에서 객체도 새로 만들지 않는다. 이 성질 하나로 클라이언트 예측, 서버 판정, 고스트 재생을 같은 함수로 처리한다.

드리프트는 “몸체만 돌리기” 로. 조향은 카트가 바라보는 방향만 돌리고 속도 벡터는 그대로 둔다. 그러면 몸체 기준 옆 속도가 생기고, 접지력이 이 옆 속도를 매 틱 지수적으로 줄인다. 줄어든 옆 속도의 일부를 앞 속도로 돌려주면 코너에서 속도를 덜 잃는 아케이드 느낌이 난다. 드리프트 중에는 접지력을 낮춰 뒤가 미끄러지는 각을 만들고, 그 미끄러짐 각이 클수록 게이지가 빨리 찬다.

// 드리프트 중 회전: 드리프트 방향으로 기본 회전 + 조향으로 더하거나 뺀다.
// 반대로 꺾어도 드리프트 방향으로 최소한은 돈다.
turnRight = d * Math.max(T.driftTurnMin, T.driftTurnBase + d * steer * T.driftTurnSteer);

충돌은 스플라인으로. 트랙이 Catmull-Rom 스플라인이라 물리 엔진을 쓰지 않고 “중심선에서 떨어진 거리 > 도로 폭 절반 − 카트 반지름” 이면 벽으로 본다. 바닥 높이와 기울기도 중심선에서 구한다.

예측과 되감기. 내 카트는 키를 누르는 즉시 클라이언트가 먼저 움직이고, 서버 상태가 오면 그 시점으로 되감은 뒤 아직 확인받지 못한 입력을 다시 돌려 맞춘다. 다른 카트는 약간 과거 시점을 보간해서 그린다. 카트끼리 부딪힘까지 예측해 보려 했는데, 측정해 보니 보정이 줄지 않고 최대 보정이 오히려 커질 때가 있어서 넣지 않았다. 출발 카운트다운처럼 서버가 멈춰 둔 구간도 서버가 알려 준 입력 번호(seq) 기준으로 맞춰야 출발 직후 튐이 없었다.

고스트 용량. 고스트는 20Hz 로 위치·방향을 기록해 16비트 정수로 양자화하고 base64 로 저장한다. 긴 기록도 수십 KB 안에 들어간다.

다음에

  • 아이템전과 랭킹을 실제 여러 사람과 돌려 보고 균형 맞추기
  • 고스트를 랭킹과 묶기