🤖
2595 in / 2000 out / 4595 total tokens
결승선 직전에서 탈것이 넘어지는 연출을 탈것별로 다르게 주는 시스템을 통째로 만들었다. devtools에서 실험하던 fallProfile 애니메이션을 게임 런타임으로 포팅한 게 핵심이다.
기존에는 결승선 접전 때 늦게 들어오는 말이 그냥 멈추기만 했다. 순위는 서버가 정해주는데 시각적으로는 누가 먼저 들어왔는지 헷갈리는 문제가 있었다. 그래서 상위 순위 말이 결승선을 아직 안 통과했으면, 나머지 말들을 결승선 15px 앞에서 넘어뜨려서 "얘는 여기서 끝"이라는 걸 시각적으로 확실히 보여주기로 했다.
horse-race-fall-motion.js를 654줄 새로 짰다. 7종 motionType(회전, 미끄러짐, 반동, 충돌 등)에 15종 탈것 프로필을 매핑했다. 자동차는 앞바퀴 들리면서 스핀, 자전거는 옆으로 미끄러지면서 쓰러짐, 로켓은 연명 폭발하면서 궤적 이탈 — 이런 식으로 각 탈것의 물리적 성격에 맞는 모션을 부여했다.
가장 고민한 건 per-vehicle 동적 버퍼다. 모든 탈것이 결승선 15px 앞에서 동일하게 정착해야 순위 배지와 시각적 통과 순서가 항상 일치한다. 탈것마다 SVG 크기가 다르고 프레임 오프셋이 달라서, 단순히 px값 하나로 고정하면 안 된다. ensureVehicleFallenState 함수에서 각 탈것의 fallen 상태를 자동 생성해서 캐싱하도록 했다.
CSS에는 .vehicle-transition-dust 레이어를 추가했다. 넘어질 때 먼지, 스파크, 스모크 트레일이 나오는 FX용 DOM이다. pointer-events: none으로 성능 이슈 방지하고, mix-blend-mode: screen으로 자연스럽게 합성되도록 했다.
horse-race-sprites.js의 getVehicleSVG 리턴값을 ensureVehicleFallenState()로 래핑하는 게 포인트다. 기존 호출부는 수정 없이도 모든 탈것이 자동으로 fallen 상태를 갖게 된다.
javascript // horse-race-sprites.js — 핵심 변경 return ensureVehicleFallenState(svgMap[vehicleId] || svgMap['car']);
이 한 줄로 15종 탈것 전체에 fallen 프로필이 즉시 생성된다. UE5에서 AnimBlueprint의 기본 상태 머신에 None → Fallen 트랜지션을 Automatic으로 걸어두는 느낌이랑 비슷하다. 런타임에 조건만 충족되면 자동 전환.
horse-race.js의 finish-stun 블록도 수정했다. 기존에는 단순히 위치를 고정했는데, 이제 animateVehicleFallState(horseElement, vehicleId, 'fallen')을 호출해서 실제 모션이 재생된다. 탈것이 넘어지는 동안 순위 배지가 위에 뜨고, 결승선 통과한 말들은 정상적으로 달려나간다. 시각적으로 "접전"이 느껴진다.
Codex가 ensureVehicleFallenState 캐시 로직과 프로필 자동 생성을 작업했다. Co-Authored-By로 Claude Opus 4.7이 찍혀 있는데, 654줄 중 상당수를 AI가 생성하고 내가 리뷰하면서 다듬은 구조다. 프롬프트로 "각 탈것의 SVG 구조를 분석해서 자연스러운 넘어짐 모션 키프레임을 생성하라"고 줬더니 꽤 쓸만한 결과가 나왔다.
한 가지 삽질 포인트. 처음에는 CSS animation으로만 처리하려다가, 탈것마다 키프레임이 다르니까 동적 스타일 시트를 계속 주입해야 한다는 걸 늦게 깨달았다. 결국 JS에서 Web Animations API로 전환했다. element.animate(keyframes, options) 형태라 탈것별로 다른 키프레임을 넘기기 훨씬 깔끔하다.
다음 할 일: 넘어진 탈것에 파티클 이펙트 추가. 지금은 CSS 트레일만 있는데, Canvas 기반 파티클로 업그레이드할 예정이다.
결승선 15px 앞, 모든 탈것이 같은 선에서 넘어진다 — 순위와 연출이 하나가 되는 지점