이 페이지는 개념 설명 — 좌표를 왜, 어떻게 눈금으로 바꾸는지만 다룬다. 서버에 적용한 결과와 A/B 실측은 별도 페이지에 있다.


1. 지금 무엇을 보내고 있나

플레이어가 움직이면 그 섹터의 이번 틱 변경분이 묶음 패킷 하나로 나간다. 그 안에 들어가는 1인분이 아래 14바이트다 (Protocol.hSectorUpdateEntry).

필드 타입 크기 실제로 쓰는 범위
playerId int32 4 B 동접 6,000명
direction uint8 1 B 5가지 (없음·상·하·좌·우)
moveState uint8 1 B 2가지 (멈춤·이동)
x float 4 B 0 ~ 120
y float 4 B 0 ~ 120
합계 14 B

여기서 1바이트를 깎으면 시야 안 사람 수만큼 곱해서 빠진다. 한 사람의 이동 한 번이 시야 안 1,010명에게 복사되기 때문이다 — MMO의 팬아웃 증폭이고, 방 하나에 64명인 FPS에는 없는 성질이다.

이 엔트리가 전체 트래픽에서 차지하는 비중이 얼마인지는 별도 실측 페이지에서 다룬다.


2. float이 왜 낭비인가

float이 담을 수 있는 것 우리가 실제로 쓰는 것
범위 ± 3.4 × 10³⁸ 0 ~ 120
정밀도 유효숫자 7자리 0.002면 충분
크기 4 바이트 2 바이트로 충분

float은 아주 큰 수와 아주 작은 수를 같은 4바이트로 다루려고 지수부와 가수부를 나눠 쓴다. 그 유연함의 대가가 4바이트인데, 우리는 0~120이라는 좁은 구간 하나만 쓰므로 대가만 치르고 혜택은 안 쓰는 셈이다.

게다가 float은 눈금 간격이 일정하지 않다. 지수부를 두는 대가로 값이 커질수록 표현할 수 있는 눈금이 듬성듬성해진다. — 1 근처에서는 간격이 0.00000012인데 100 근처에서는 0.0000076으로 64배 벌어진다. 같은 맵 안에서도 원점 쪽은 촘촘하고 끝은 듬성등성하다. 반면 고정소수점은 어디서나 간격이 같다.

맵이 120이라 지금은 양쪽 다 넉넉해 차이가 안 난다. 하지만 맵이 10만 단위로 커지면 float 간격은 0.008까지 벌어져 1/512 눈금(0.002)보다 오히려 듬성듬성하다. 대형 오픈월드가 원점에서 멀어질수록 위치가 떨리는 게 이 때문이고, 언리얼이 좌표를 double로 넓히거나(LWC) 원점 자체를 옮기는(floating origin) 이유다.

이건 설계 결정이 아니라 개발 편의였다 — 서버 내부 표현(float)을 그대로 와이어에 태웠을 뿐이다. 언리얼이 FVector_NetQuantize를 타입으로 제공하고, Quake 3가 좌표를 1/8 단위 정수로 보낸 것처럼, 네트워크 게임에서 좌표를 그대로 보내지 않는 건 표준 관행이다.

바꾸는 건 와이어뿐이다. 서버 내부 계산은 float 그대로 둔다 — 이동은 speed × deltaTime 곱셈이라 소수가 필연이고, 정수로 누적하면 나머지가 한쪽으로만 쌓인다. 이동·섹터 판정·벽 판정을 전혀 건드리지 않으므로 게임 로직에는 영향이 없다.


3. 눈금으로 바꾸기 — 고정소수점

바닥에 일정 간격으로 눈금을 긋고 번호만 보낸다. 받는 쪽은 간격을 알고 있으니 되돌릴 수 있다. 이렇게 연속된 값을 정해진 눈금으로 끊는 것을 양자화라 하고, 그중 눈금 간격을 상수로 고정한 방식이 고정소수점이다(다른 종류는 5절).

보낼 때   qx = (uint16)(x * 512 + 0.5f)   // 반올림
받을 때   x  = qx * (1.0f / 512)

예)  63.7  →  32,614  →  63.6992   (오차 0.0008)