프레임 사이로 빠져나간 뿌리 — 충돌과 터널링
충돌 판정은 두 종류로 나뉜다.
당근이나 바위 같은 점 모양 엔티티는 구로 근사해서, 플레이어와의 3D 거리가 반경 합보다 작으면 닿은 것으로 본다.
단순하지만 충분했다.

실제 값은 토끼 0.45, 당근 0.35, 바위 0.7이다.
당근은 먹어야 하니 후하게, 바위는 피해야 하니 크게 잡았다.
같은 구 근사라도 반경 하나로 "잘 먹히는 것"과 "무서운 것"이 갈린다.
호 모양 장애물
까다로운 건 뿌리였다.
뿌리는 점이 아니라 터널 둘레를 가로지르는 호 모양 장애물이다.
그래서 세 조건을 모두 만족할 때만 충돌이다 — 깊이가 가깝고, 토끼 각도가 뿌리의 호 범위 안에 있고, 점프 높이가 뿌리를 못 넘었을 때.
충분히 높이 뛰면 뿌리 위로 통과한다.
통과 높이는 1.1m로 잡혀 있다.
일반 점프 정점이 약 1.69m니 점프만 하면 넉넉히 넘는다.
뿌리는 "반응해서 뛰어야 하는" 장애물이지 "정확히 타이밍을 맞춰야 하는" 장애물이 아니다.
프레임 사이로 빠져나간 뿌리
여기서 진짜 버그가 나왔다.
빠른 속도에서 뿌리를 그냥 뚫고 지나가버렸다.
원인은 프레임당 이동량이었다.
최고속도 30m/s에서 화면이 20fps로 떨어지면 한 프레임에 1.5m를 건너뛴다.
그런데 뿌리의 깊이 판정 창은 그보다 좁았다(1.46m).
토끼가 한 프레임 전엔 뿌리 앞, 다음 프레임엔 뿌리 뒤 — 정작 겹치는 순간은 두 프레임 사이로 빠져버린다.
고전적인 터널링이다.
해결은 판정 창에 이번 프레임 이동량(sweep)을 더해주는 것이었다.
export function hitsRoot(playerDepth, p, rootDepth, root, sweep = 0): boolean {
if (Math.abs(playerDepth - rootDepth) > SPAWN.ROOT_TUBE + PLAYER.RADIUS + sweep)
return false;
if (p.height >= SPAWN.ROOT_CLEARANCE) return false;
return angularDist(p.angle, root.angle) < root.arc / 2 + PLAYER.RADIUS / TUNNEL.RADIUS;
}
이러면 빨리 지나갈수록 판정 창이 그만큼 넓어져, 점프하지 않은 한 뿌리를 놓치지 않는다.
프레임 속도가 떨어질수록 창이 넓어지는 것이라, 느린 기기에서 오히려 판정이 헐거워지는 문제도 같이 사라진다.
판정과 그림을 일부러 어긋나게
뿌리에는 반경이 두 개 있다.
충돌 판정에 쓰는 값은 0.28이고, 화면에 그리는 값은 0.36이다.
약 29% 더 두껍게 그린다.
의도한 불일치다.
판정 그대로 얇게 그렸더니 뿌리가 배경의 흙 무늬처럼 보여서, "넘어야 하는 것"이라는 신호가 약했다.
굵게 그리면 한눈에 장애물로 읽힌다.
그리고 방향이 중요하다.
그림이 판정보다 크기 때문에, 플레이어 입장에서는 "닿을 것 같았는데 안 맞은" 경우만 생기고 "안 닿았는데 맞은" 억울함은 생기지 않는다.
대신 판정값은 절대 건드리지 않는다는 규칙을 붙였다.
시각 반경은 가독성 손잡이고, 판정 반경은 게임의 규칙이다.
둘을 같은 상수로 묶으면 "보기 좋게" 조정하는 순간 난이도가 조용히 바뀐다.
니어미스는 지나간 뒤에 잰다
니어미스(+50점)에도 요령이 있었다.
다가올 때가 아니라 "지나간 뒤" 단면 거리로 판정한다.
부딪히진 않았는데 아슬아슬하게 스친 순간을, 통과 직후에 재서 보너스를 준다.
이 한 끗이 회피의 쾌감을 점수로 보상한다.
기준은 반경 합보다는 멀고, 거기서 0.6m 안쪽일 때다.
접근 중에 재면 결국 피할지 말지 모르는 상태라 판정이 성립하지 않는다.
결과가 확정된 뒤에 재야 "스쳤다"가 의미를 갖는다.
뿌리는 방식이 다르다.
뿌리의 니어미스는 통과 높이 1.1m 위로 0.6m 구간, 그러니까 1.1에서 1.7m 사이로 넘어갔을 때만 준다.
높이 날면 안전한 대신 보너스가 없고, 낮게 스치듯 넘으면 위험한 대신 점수가 붙는다.
그냥 점프만 눌러도 넘어지는 장애물에 "잘 넘는 법"을 하나 더 만들어준 셈이다.
순수 함수의 값
이 판정들은 전부 인자를 받아 불리언을 돌려주는 함수다.
Three.js 객체도, 씬 그래프도 참조하지 않는다.
그래서 터널링 버그를 재현하는 테스트를 그냥 짤 수 있었다.
큰 dt와 높은 속도를 넣고 뿌리를 지나가게 한 뒤 충돌이 잡히는지 확인하면 끝이다.
실제 20fps 상황을 만들어낼 필요가 없다.
다음 편은 마지막, 효과음을 코드로 합성한 이야기다.