맵이 아주 넓은 게임에서는 원점에서 멀어질수록 캐릭터의 움직임이 이상해지는 경우가 있는데요. 부동소수점 정밀도 때문이라는 설명은 어디서나 찾아볼 수 있는데, 얼마나 멀리 가야 얼마나 틀어지는지 숫자로 보여 주는 글은 찾지 못했습니다.
그래서 직접 재 봤는데요. 걷는 속도가 0.1m/s부터 6m/s까지 제각각인 가상 캐릭터 8만 4천 명을 원점에서 100km 떨어진 곳에 세우고 10초 동안 걷게 했더니 20.7%는 한 걸음도 움직이지 못한다는 결론이 나왔습니다. 아래에서 맵 끝으로 갈수록 캐릭터가 불안정해지는 까닭에 대해 자세히 알아보도록 합시다.
멀어질수록 숫자 간격이 벌어진다
컴퓨터가 위치를 저장하는 32비트 부동소수점(float32)은 유효 숫자를 24비트까지만 담습니다. 위키백과의 단정밀도 항목을 보면 2의 n제곱에서 n+1제곱 사이는 이웃한 값의 간격이 늘 2의 (n−23)제곱으로 같죠. 값이 커지면 간격도 같이 벌어진다는 말입니다.
좌표 한 칸을 1m로 잡으면 100km는 값이 100,000이 되는데, 이때 간격이 2의 −7제곱, 약 0.0078m입니다. 같은 식으로 구하면 1km는 0.061mm, 10km는 0.98mm, 1,000km는 62.5mm가 나옵니다. 고닷(Godot) 한글 문서도 큰 월드에서는 부동소수점의 한계가 문제가 될 수 있고 값이 0에 가까울수록 정밀도가 가장 높다고 안내하고 있죠.
100km에서 멈춘 캐릭터
처음에는 캐릭터 속도를 전부 똑같이 줬는데, 8만 4천 명이 한 명처럼 같은 결과를 내서 인원수가 아무 의미가 없었어요. 그래서 속도와 시작 위치를 무작위로 바꿔 다시 돌려버렸습니다. 이동은 매 프레임 위치에 속도×(1/60초)를 더하는 가장 단순한 방식으로, float32에서 600번(10초) 반복했고요.
시작 위치는 기준 거리의 ±10% 안에서 흩어 놓았습니다. 1km에서는 이동 거리 오차의 중앙값이 0.12%였고 10% 넘게 틀린 캐릭터는 하나도 없었는데요. 10km부터는 열 명 중 한 명쯤(9.3%)이 10% 넘게 틀렸고, 50km에서는 3.9%가 아예 멈춰버렸습니다. 100km에서는 20.7%가 멈추고 55.3%가 틀렸어요. 거리가 더 늘면 금방 나빠져서 1,000km에서는 76.2%가 멈췄고 96.3%가 틀렸습니다. 세 번 돌린 값의 차이는 0.3%p 이내였습니다.

멈추는 기준은 반 칸이었다
이 숫자가 우연이 아닌지 따져 봤는데요. 더하려는 값이 간격의 절반보다 작으면 가장 가까운 값으로 반올림되어 위치가 그대로 남습니다. 100km에서 간격의 절반은 3.9mm고, 60프레임으로 환산하면 초속 0.234m보다 느린 캐릭터는 제자리에 서게 됩니다.
속도의 로그가 고르게 나오도록 0.1과 6 사이를 뽑았으니 0.1에서 0.234 사이에 들어갈 비율은 ln(2.34)÷ln(60), 약 20.8%였습니다. 실측 20.7%와 거의 같은 것이죠. 50km에서도 기준 속도는 0.117m/s이고 계산값 3.9%가 실측 3.9%와 맞았습니다.
그림의 검정 계단이 간격의 절반이고 점선이 각 속도의 한 프레임 이동량입니다. 계단이 점선 위로 올라서는 지점부터 그 속도의 캐릭터는 움직이지 못하게 됩니다.
빨라지는 캐릭터도 있다?
반올림은 멈추게만 하지 않습니다. 한 프레임 이동량이 간격 사이 어딘가에 놓이면 위 칸이나 아래 칸으로 붙기 때문에 속도 자체가 달라집니다. 100km에서 10% 넘게 느려진 캐릭터는 32.5%(멈춘 20.7% 포함), 10% 넘게 빨라진 캐릭터는 22.8%였어요.
정확히 100km 지점에서 0.3m/s로 걷게 하면 한 프레임에 5mm가 아니라 7.8mm씩 나아가서, 10초 이동 거리가 정답보다 56% 길었습니다. 떨림이 보일 줄 알았는데 일정한 속도로 걸을 때는 프레임마다 이동량이 거의 같았습니다.
제 생각에는 이번 결과에 가장 잘 맞는 말이 불안정입니다. 덜덜 떨기보다 어떤 캐릭터는 서 버리고, 어떤 캐릭터는 정해진 속도보다 빨리 가거나 느리게 갔기 때문입니다.
원점을 옮기면 없어졌다
같은 이동을 원점에서 시작하면 오차 중앙값은 0.0004%, 최대는 0.0009%였는데요. 1,000km 지점에서도 위치를 64비트(float64)로 저장하면 최대 오차가 0.0000069%에 그쳤습니다. 캐릭터 근처로 원점을 옮기거나 위치를 더 넓은 자릿수로 저장하면 이 문제는 계산상 사라진 셈이라고 얘기할 수 있습니다.
그러나 이 실험은 일정한 속도의 이동 하나만 다뤘고, 렌더링 정점이나 물리 엔진 내부 계산, 엔진별 구현은 확인하지 못했다는 한계가 있습니다. 좌표 한 칸을 1m로 본 것과 속도 범위도 제가 정한 가정이기 때문이죠. 그래도 맨 앞의 20.7%는 버그가 아니라 반올림이 만든 정확한 비율이었다고 단언할 수 있습니다.