시작은 단순한 최고속 문제처럼 보였다
지난 글에서는 도로 양옆의 구조를 다시 만들고, 가드레일 충돌과 FT86 기반 파워밴드의 첫 기준을 잡았다. 이번 글은 그 후속편이다. 숫자상으로는 그럴듯해진 차량을 실제 코스와 자동 측정에 올렸을 때 어떤 문제가 다시 드러났는지 다룬다.
Apex Seoul의 FT86 기반 차량인 Raven Coupe에는 최고속 225km/h가 표시돼 있었다. 실제 기어비, final drive, 타이어 둘레를 적용했고 RPM도 그에 맞춰 올라갔다. 그렇다면 긴 직선에서는 자연스럽게 225km/h에 가까워져야 했다.
하지만 내부 주행 QA에서 차량은 약 1분을 달린 뒤에도 140.7km/h 부근에 머물렀다. 코너 시나리오에서는 도로 중앙에서 조금만 벗어나도 가드레일 충돌이 발생했고, 드리프트 종료 가속도 자동 측정에서는 관찰되지 않았다.
처음에는 모두 “차량이 기대한 만큼 움직이지 않는다”는 한 가지 문제처럼 보였다. 실제로는 서로 다른 세 좌표계와 시간 조건이 겹친 결과였다.
| 보이는 현상 | 실제 원인 | 확인해야 했던 기준 |
|---|
| 225km/h 차량이 140km/h에서 정체 | 물리 RPM 위에 예전 속도 제한과 과도한 공기저항이 남음 | 구동력과 저항의 평형 |
| 도로 안쪽인데 가드레일 충돌 | 화면 픽셀 크기를 물리 거리로 역변환 | 월드 좌표 충돌 경계 |
| 물리 경계 수정 후 차가 화면 밖으로 붙음 | 충돌은 맞지만 차량과 도로를 서로 다른 깊이로 투영 | 차량이 실제 그려지는 Y의 도로 폭 |
| 드리프트 종료 가속이 보이지 않음 | QA 입력이 너무 늦고 판정이 한 프레임에 의존 | 이벤트 순서와 허용 시간 |
수정 과정은 한 번에 끝나지 않았다. 최고속을 고치자 잘못된 가드레일 충돌이 드러났고, 충돌을 고치자 차량이 화면 밖으로 밀려 보였다. 마지막에는 구현이 아니라 QA 입력 순서가 잘못됐다는 사실도 확인했다. 이 글은 그 실패가 드러난 순서대로 원인과 다음 시도를 정리한다.
1. 최고속 숫자와 도달 가능한 최고속은 다르다
차량 JSON에 225km/h가 적혀 있다고 해서 차량이 그 속도에 도달하는 것은 아니다.
쉽게 말하면 엔진은 차를 앞으로 밀고, 타이어와 공기는 차를 뒤로 잡아당긴다. 내리막은 조금 더 밀어주고 오르막은 반대로 속도를 깎는다. 앞으로 미는 힘과 방해하는 힘이 같아지는 순간부터 속도는 더 올라가지 않는다. 화면에 표시한 최고속보다 이 균형점이 낮으면 차량은 그 숫자에 도달할 수 없다.
아래 수식은 이 설명을 계산에 사용하기 위해 한 줄로 정리한 것이다. 수식을 읽지 않아도 핵심은 앞으로 미는 쪽과 방해하는 쪽의 합이 0이 되는 속도가 실제 자연 최고속이라는 점이다.
net force = drive force + rolling resistance + aero drag + slope force
평지에서 net force = 0이 되는 속도가 자연 최고속이다. 안전을 위한 속도 제한은 그 이후에 개입해야 한다. 둘을 섞으면 화면에는 225km/h가 있어도 차량은 훨씬 낮은 속도에서 멈출 수 있다.
물리 기어비와 예전 속도 구간이 동시에 작동하고 있었다
Raven Coupe는 게임 안의 이름이고, 물리 기준 차량은 FT86이다. 두 차량을 따로 취급한 것이 아니라 FT86의 실제 기어비, final drive, 타이어 회전수를 Raven Coupe 설정에 적용했다.
문제는 새 RPM 계산을 넣은 뒤에도 예전의 단순 속도 기반 기어 구간이 남아 있었다는 점이다. 예를 들어 예전 4단 경계는 약 135km/h였지만, 물리 기어비로 계산한 4단 7,400rpm은 약 175.34km/h다.
즉 엔진은 물리 RPM으로 회전하지만 일부 가속 판단은 이전 속도표를 따라갔다. 계기판 숫자는 그럴듯했지만 힘이 전달되는 구간은 서로 맞지 않았다.
225km/h에서 미는 힘과 방해하는 힘을 따로 봤다
어느 쪽이 문제인지 알기 위해 225km/h 상태를 강제로 만든 뒤, 속도를 올리는 항목과 낮추는 항목을 따로 기록했다. 결과는 간단했다. 엔진이 18.343만큼 밀어주는 동안 공기저항만 69.312만큼 속도를 깎고 있었다.
세부 측정값은 다음과 같다.
| 항목 | 힘 |
|---|
| 구동력 | +18.343 |
| 구름 저항 | -14.000 |
| 공기저항 | -69.312 |
| 합계 | -64.969 |
225km/h에서는 가속 여유가 조금 부족한 정도가 아니라, 공기저항이 구동력을 압도하고 있었다. 이 상태에서 안전 최고속만 225로 올려도 차는 그곳까지 도달할 수 없다.
첫 번째 실패: 엔진 전체를 4.5배 강하게 만들기
225km/h에서 힘의 평형을 맞추기 위해 엔진 출력을 전 구간에서 약 4.542배 올리는 방법도 계산해 봤다. 최고속에는 접근할 수 있었지만 0–100km/h가 약 1.317초로 무너졌다.
최고속 하나를 맞추려고 저속 토크까지 키운 전형적인 과보정이었다. 그래서 엔진의 성격은 유지하고 고속에서만 지배적인 공기저항 계수를 조정했다.
aero drag coefficient
0.00012 -> 0.000007661283
그 결과 평지의 자연 평형 속도는 223.953km/h가 됐다. 225km/h는 여전히 시스템이 허용하는 최고속으로 남지만, 평지에서는 강제 제한에 기대지 않고 목표 속도 바로 아래에서 힘이 균형을 이룬다.
출발 가속은 별도로 확인했다. launch curve를 복원한 뒤 결정론적 0–100km/h는 약 8.08~8.12초로 돌아왔다. 최고속 보정이 저속 가속을 다시 망가뜨리지 않았다는 뜻이다.
런타임에서는 경사까지 구분해야 했다
내부 QA의 직선 시나리오는 완전한 평지가 아니라 완만한 내리막이다. 이곳에서는 경사력이 조금 더해져 차량이 225km/h 최고속 제한에 닿는다. 이는 평지 자연 최고속 223.953km/h와 모순되지 않는다.
- 평지: 구동력과 저항이 약
223.953km/h에서 자연 평형
- 완만한 내리막: 남은 경사력으로
225km/h 최고속 제한 도달

이 구분이 없으면 “225km/h에 도달했다”는 한 장면만 보고 물리 평형까지 맞았다고 오해하기 쉽다.
2. 화면에서 보이는 가드레일과 충돌하는 가드레일
최고속을 고친 뒤 일반 코너링과 드리프트 시나리오를 다시 돌리자 새로운 문제가 더 선명해졌다. 차량은 물리 도로 폭의 약 12~14%만 이동했는데도 가드레일 충돌로 판정됐다.

당시 계산은 세 종류의 값을 섞고 있었다.
- 세그먼트에 기록된 물리 도로 폭
- 원근 투영된 화면 도로 폭
- 차량 스프라이트의
displaySize와 anchor scale
화면에서 몇 픽셀로 보이는지를 역산해 물리 충돌 폭으로 만들고, 그 값과 원래 물리 폭 중 작은 쪽을 선택했다. 서로 다른 깊이에서 나온 scale까지 섞였기 때문에 실제 물리 가드레일은 약 961~1086u인데 충돌 한계는 약 130u까지 줄어들었다.
첫 번째 시도: 충돌은 화면이 아니라 실제 도로 폭으로 계산한다
첫 번째 수정에서는 화면 값을 충돌 계산에서 완전히 제외했다.
쉽게 말하면 포장도로 폭에 갓길을 더해 가드레일 위치를 정하고, 여기서 차량 반폭을 빼 차량 중심이 갈 수 있는 끝점을 구했다. 화면에서 차가 몇 픽셀로 보이는지는 이 계산에 사용하지 않는다. 아래 수식은 이 규칙을 코드에 옮기기 위한 표현이다.
rail line = paved half width + shoulder clearance
vehicle center limit = rail line - physical vehicle half width
FT86 기준값은 다음과 같이 정리했다.
| 항목 | 값 |
|---|
| 갓길 여유 | 220u |
| 차량 물리 반폭 | 240u |
| 넓은 도로 포장 반폭 | 960u |
| 넓은 도로 rail line | 1180u |
| 넓은 도로 차량 중심 한계 | 940u |
| 좁은 도로 포장 반폭 | 820u |
| 좁은 도로 차량 중심 한계 | 800u |
화면 크기 역변환과 좌우 보정값 48/56은 제거했다. 렌더러의 가드레일 오브젝트도 같은 세그먼트 도로 폭과 220u 갓길을 사용하도록 맞췄다.
이후 일반 코너링과 드리프트의 거짓 충돌은 모두 0회가 됐다. 물리 충돌 경계만 놓고 보면 1차 수정은 성공이었다.
그런데 화면에서는 차가 가드레일 밖으로 나갔다

이 장면은 중요한 반례였다. 물리 좌표가 맞아도 화면 투영이 맞는 것은 아니다.
1차 수정의 차량 X는 플레이어 앞 260u 지점의 projection scale로 계산했다. 하지만 차량 스프라이트는 화면 아래쪽의 거의 고정된 Y에 합성된다. 도로는 깊이에 따라 폭이 달라지므로, 앞쪽 도로의 scale로 아래쪽 차량 위치를 계산하면 실제 차량이 놓인 지점의 도로 폭과 맞지 않는다.
결국 X가 화면 밖으로 나가고 마지막에 화면 경계 clamp가 걸렸다. 충돌 로그는 정상인데 눈으로는 차가 가드레일을 뚫은 것처럼 보였다.
두 번째 시도: 차량이 실제 그려지는 높이에서 도로 폭을 다시 잰다
두 번째 수정에서는 차량이 합성되는 화면 Y에서 좌우 도로 경계를 직접 샘플링했다.
쉽게 말하면 물리적으로 도로의 절반만큼 왼쪽으로 이동한 차는 화면에서도 현재 보이는 도로 폭의 절반만큼 왼쪽에 놓이게 했다. 차량이 실제 가드레일 한계에 도달했을 때만 화면의 차체 외곽도 가드레일에 닿는다. 아래 수식은 그 비율 변환을 단계별로 적은 것이다.
road screen scale = road span px / physical paved width
screen rail = paved edge +/- shoulder px
normalized offset = physical lateral offset / physical rail center limit
vehicle center = lerp(road center, side contact center, normalized offset)
핵심은 물리 횡방향 비율을 먼저 구하고, 그 비율을 현재 화면 Y의 도로 폭에 적용하는 것이다. 차량이 물리 한계 -1 또는 +1에 도달하면 차량의 접촉 외곽선이 화면 가드레일과 정확히 만난다.
화면 접촉 폭에는 투명 여백이 포함된 전체 sprite cell 대신, FT86 atlas 중앙 그림자의 실제 차체 폭을 사용했다. 충돌 물리 폭과 표현용 접촉 폭은 목적이 다르므로 이름과 측정 기준도 분리했다.

좌우 끝점은 눈대중 대신 식으로 검증했다
왼쪽 물리 한계 -940u를 강제로 입력했을 때 화면 가드레일과 차량 접촉 외곽은 모두 211.3216px이었다.

오른쪽 +940u에서는 두 값이 모두 1067.9557px이었다.

두 경우 모두 간격은 0px이었다. 일반 코너링과 드리프트 샘플에서도 음수 간격, 즉 화면상 가드레일 침범은 나오지 않았다.
3. 드리프트 QA가 물리를 잘못 고치게 만들 뻔했다
마지막으로 남은 것은 드리프트 종료 가속이었다. 처음 자동 시나리오에서는 counter steer timer와 exit burst가 모두 관찰되지 않았다. 구현이 고장 난 것처럼 보였지만 텔레메트리의 이벤트 순서를 펼쳐 보니 QA 입력 자체가 문제였다.
초기 시나리오는 브레이크를 0.3~0.8초 동안 유지했다. 반대 조향을 넣기도 전에 속도가 drift sustain 기준인 약 95km/h 아래로 떨어졌다. 이미 드리프트가 끝난 뒤였기 때문에 counter steer를 아무리 넣어도 timer가 생길 수 없었다.
입력 순서를 실제 운전 흐름에 가깝게 바꿨다
0.45s 브레이크 해제
0.55s 왼쪽 조향에서 오른쪽 counter steer로 전환
1.05s 조향과 스로틀 중립
1.45s 가속 재입력
수정된 시나리오에서는 counter steer timer 0.749초, trim 0.62가 관찰됐다.
exit burst에는 또 하나의 프레임 경계 문제가 있었다. recovery -> grip 전환이 브라우저 캡처 한 간격 안에서 끝날 수 있는데, 기존 판정은 바로 그 전환 프레임에 가속 페달이 눌려 있어야 했다. 사람이 보기에는 즉시 다시 가속했어도 측정 프레임이 한 번 어긋나면 이벤트가 사라졌다.
그래서 grip 복귀 뒤 0.28초의 재가속 허용 창을 추가했다. 즉시 재가속하는 기존 동작은 유지하면서, 짧은 캡처 간격 때문에 이벤트를 놓치는 문제만 흡수했다. 최종 런타임에서 exit burst 최대값은 0.0354로 확인됐다.

이 작업에서 얻은 교훈은 단순하다. 자동 QA 시나리오는 구현을 검사하는 도구이면서 동시에 동작 명세다. 시나리오의 입력 순서가 틀리면 정상 물리를 버그로 판정하고, 그 오진을 기준으로 다시 물리를 망가뜨릴 수 있다.
4. 최종 내부 QA 결과
| 시나리오 | 샘플 | 속도 범위 | 관찰 상태 | 최대 shader 강도 | rail impact |
|---|
| 직선 가속 | 700 | 0.2~225.0km/h | 1~6단, 자연 가속 후 내리막 최고속 제한 | 0.1056 | 0 |
| 일반 코너링 | 90 | 113.0~193.6km/h | 접지 유지, 좌우 화면 접촉 정상 | 0.1089 | 0 |
| 드리프트 | 80 | 134.7~191.5km/h | 진입 → 드리프트 → 회복 → 접지 | 0.1339 | 0 |
자동 검사에서 더 이상 차단 항목이 발견되지 않았다. 다만 이것은 자동 검증을 통과했다는 뜻이다. 실제 주행에서의 속도감과 조향 감각은 이번 글의 승인 범위에 포함하지 않았으며, 다음 작업에서 직접 주행하며 평가할 예정이다.
5. 이번 작업에서 분리한 다섯 가지 테스트
처음에는 하나의 긴 주행으로 모든 것을 확인하려 했다. 지금은 실패 유형에 따라 테스트를 분리한다.
-
정지 상태 출발 테스트
0–100km/h와 변속 시점을 본다. 출발 가속 설정과 저속 엔진 힘이 대상이다.
-
속도를 올리는 힘과 방해하는 힘 비교
특정 속도에서 엔진이 미는 힘, 타이어 저항, 공기저항, 경사의 영향을 각각 출력한다. 어느 항목이 속도를 막는지 찾는다.
-
평지 자연 최고속 테스트
강제 최고속 제한 없이 앞으로 미는 힘과 방해하는 힘이 같아지는 속도를 측정한다.
-
실제 코스 자동 주행
실제 코스의 경사, 코너, 입력 순서, 화면 효과 반응을 함께 확인한다.
-
가드레일 화면 접점 검사
물리 좌우 한계를 강제로 넣었을 때 차량 접촉 외곽과 화면 가드레일의 좌표가 일치하는지 검사한다.
같은 차량을 검사하더라도 질문이 다르면 측정 장치도 달라야 한다. 0–100km/h를 맞추는 도구로 최고속 평형을 고칠 수 없고, 화면 스크린샷만으로 물리 충돌 폭을 결정할 수도 없다.
마무리: 보이는 좌표와 움직이는 좌표를 분리하기
이번 사이클의 가장 큰 변경은 특정 계수를 조정한 것이 아니다. 물리 좌표와 화면 좌표의 책임을 분리한 것이다.
- 기어비와 타이어 회전수는 RPM과 구동력을 결정한다.
- 구동력과 저항의 평형은 자연 최고속을 결정한다.
- 세그먼트 폭과 차량 물리 폭은 충돌을 결정한다.
- 차량이 그려지는 화면 Y의 도로 폭은 스프라이트 위치를 결정한다.
- QA의 입력 순서와 시간 창은 상태 전환을 검증한다.
Pseudo 3D에서는 화면에 보이는 크기가 계속 변한다. 그래서 픽셀을 다시 물리 단위로 바꾸고 싶은 유혹이 크다. 하지만 그 픽셀이 어떤 깊이에서 계산됐는지 놓치는 순간, 도로 중앙에서 가드레일에 부딪히거나 물리적으로 정상인 차가 화면 밖으로 나간다.
반대로 물리 좌표를 단 하나의 기준으로 유지하고, 마지막 표현 단계에서만 현재 화면의 도로 폭에 매핑하면 두 세계를 함께 검증할 수 있다. 숫자가 맞는 것과 장면이 맞는 것을 각각 검사한 뒤, 마지막에 둘의 접점을 식으로 고정하는 방식이다.
최고속 225km/h를 구현하는 데 필요했던 것은 더 강한 엔진 하나가 아니었다. 속도표, 힘의 평형, 월드 충돌, 화면 투영, QA 시간축을 서로 다른 문제로 인정하는 일이었다.